@nitra/zebra 6.1.4 → 6.1.5

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.
@@ -0,0 +1,1345 @@
1
+ package com.nitra.zebra_printer_plugin;
2
+
3
+ import android.Manifest;
4
+ import android.bluetooth.BluetoothAdapter;
5
+ import android.bluetooth.BluetoothDevice;
6
+ import android.bluetooth.BluetoothGatt;
7
+ import android.bluetooth.BluetoothGattCallback;
8
+ import android.bluetooth.BluetoothGattCharacteristic;
9
+ import android.bluetooth.BluetoothGattService;
10
+ import android.bluetooth.BluetoothManager;
11
+ import android.bluetooth.BluetoothProfile;
12
+ import android.bluetooth.BluetoothSocket;
13
+ import android.bluetooth.le.BluetoothLeScanner;
14
+ import android.bluetooth.le.ScanCallback;
15
+ import android.bluetooth.le.ScanResult;
16
+ import android.content.BroadcastReceiver;
17
+ import android.content.Context;
18
+ import android.content.Intent;
19
+ import android.content.IntentFilter;
20
+ import android.content.pm.PackageManager;
21
+ import android.os.Build;
22
+ import android.os.Handler;
23
+ import android.os.Looper;
24
+ import android.util.Log;
25
+
26
+ import androidx.core.app.ActivityCompat;
27
+ import androidx.core.content.ContextCompat;
28
+
29
+ import com.getcapacitor.JSObject;
30
+ import com.getcapacitor.Plugin;
31
+ import com.getcapacitor.PluginCall;
32
+ import com.getcapacitor.PluginMethod;
33
+ import com.getcapacitor.annotation.CapacitorPlugin;
34
+ import com.getcapacitor.annotation.Permission;
35
+
36
+ import java.io.IOException;
37
+ import java.io.OutputStream;
38
+ import java.lang.reflect.Method;
39
+ import java.util.ArrayList;
40
+ import java.util.List;
41
+ import java.util.UUID;
42
+ import java.util.concurrent.ExecutorService;
43
+ import java.util.concurrent.Executors;
44
+ import java.util.concurrent.Future;
45
+ import java.util.concurrent.TimeUnit;
46
+ import java.util.concurrent.TimeoutException;
47
+
48
+ @CapacitorPlugin(name = "ZebraPrinter", permissions = {
49
+ @Permission(alias = "bluetooth", strings = {
50
+ Manifest.permission.BLUETOOTH,
51
+ Manifest.permission.BLUETOOTH_ADMIN,
52
+ Manifest.permission.BLUETOOTH_CONNECT,
53
+ Manifest.permission.BLUETOOTH_SCAN,
54
+ Manifest.permission.ACCESS_FINE_LOCATION,
55
+ Manifest.permission.ACCESS_COARSE_LOCATION
56
+ })
57
+ })
58
+ public class ZebraPrinter extends Plugin {
59
+
60
+ private static final String TAG = "ZebraPrinterPlugin";
61
+
62
+ // SPP UUID for classic Bluetooth
63
+ private static final UUID SPP_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
64
+
65
+ // BLE UUIDs for Zebra printers
66
+ private static final UUID ZEBRA_SERVICE_UUID = UUID.fromString("0000FF00-0000-1000-8000-00805F9B34FB");
67
+ private static final UUID ZEBRA_CHARACTERISTIC_UUID = UUID.fromString("0000FF01-0000-1000-8000-00805F9B34FB");
68
+
69
+ private BluetoothManager bluetoothManager;
70
+ private BluetoothAdapter bluetoothAdapter;
71
+ private BluetoothLeScanner bluetoothLeScanner;
72
+ private BluetoothGatt bluetoothGatt;
73
+ private BluetoothGattCharacteristic printerCharacteristic;
74
+
75
+ private List<BluetoothDevice> discoveredDevices = new ArrayList<>();
76
+ private boolean isScanning = false;
77
+ private boolean isConnected = false;
78
+ private String connectedPrinterAddress;
79
+
80
+ // MTU size for BLE
81
+ private static final int BLE_MTU_SIZE = 20;
82
+
83
+ // CRITICAL FIX: Async chunk sending system
84
+ private boolean isWriting = false;
85
+ private byte[] currentData;
86
+ private int currentOffset = 0;
87
+ private PluginCall currentPrintCall;
88
+
89
+ // SPP support for fallback
90
+ private BluetoothSocket sppSocket;
91
+ private OutputStream sppOutputStream;
92
+ private boolean useSPP = false;
93
+
94
+ @Override
95
+ public void load() {
96
+ super.load();
97
+ Log.d(TAG, "🔧 Loading NitraZebraPlugin...");
98
+
99
+ bluetoothManager = (BluetoothManager) getContext().getSystemService(Context.BLUETOOTH_SERVICE);
100
+ bluetoothAdapter = bluetoothManager.getAdapter();
101
+
102
+ if (bluetoothAdapter != null) {
103
+ bluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner();
104
+ Log.d(TAG, "✅ Bluetooth adapter initialized");
105
+
106
+ // Register Classic Bluetooth discovery receiver
107
+ IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
108
+ filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
109
+ getContext().registerReceiver(bluetoothReceiver, filter);
110
+ Log.d(TAG, "✅ Classic Bluetooth discovery and bonding receiver registered");
111
+ } else {
112
+ Log.e(TAG, "❌ Bluetooth not supported");
113
+ }
114
+ }
115
+
116
+ @PluginMethod
117
+ public void checkPermissions(PluginCall call) {
118
+ Log.d(TAG, "🔍 Checking permissions...");
119
+
120
+ boolean hasAllPermissions = true;
121
+ List<String> missingPermissions = new ArrayList<>();
122
+
123
+ // Check basic Bluetooth permissions
124
+ if (ContextCompat.checkSelfPermission(getContext(),
125
+ Manifest.permission.BLUETOOTH) != PackageManager.PERMISSION_GRANTED) {
126
+ hasAllPermissions = false;
127
+ missingPermissions.add(Manifest.permission.BLUETOOTH);
128
+ }
129
+
130
+ if (ContextCompat.checkSelfPermission(getContext(),
131
+ Manifest.permission.BLUETOOTH_ADMIN) != PackageManager.PERMISSION_GRANTED) {
132
+ hasAllPermissions = false;
133
+ missingPermissions.add(Manifest.permission.BLUETOOTH_ADMIN);
134
+ }
135
+
136
+ // Check location permissions (required for BLE scanning)
137
+ if (ContextCompat.checkSelfPermission(getContext(),
138
+ Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
139
+ hasAllPermissions = false;
140
+ missingPermissions.add(Manifest.permission.ACCESS_FINE_LOCATION);
141
+ }
142
+
143
+ // Check Android 12+ Bluetooth permissions
144
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
145
+ if (ContextCompat.checkSelfPermission(getContext(),
146
+ Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
147
+ hasAllPermissions = false;
148
+ missingPermissions.add(Manifest.permission.BLUETOOTH_CONNECT);
149
+ }
150
+
151
+ if (ContextCompat.checkSelfPermission(getContext(),
152
+ Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) {
153
+ hasAllPermissions = false;
154
+ missingPermissions.add(Manifest.permission.BLUETOOTH_SCAN);
155
+ }
156
+ }
157
+
158
+ JSObject result = new JSObject();
159
+ result.put("hasPermissions", hasAllPermissions);
160
+ result.put("missingPermissions", missingPermissions.toArray(new String[0]));
161
+ result.put("bluetoothSupported", bluetoothAdapter != null);
162
+ result.put("bleSupported",
163
+ getContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE));
164
+
165
+ call.resolve(result);
166
+ }
167
+
168
+ @PluginMethod
169
+ public void requestPermissions(PluginCall call) {
170
+ Log.d(TAG, "🔐 Requesting permissions...");
171
+
172
+ List<String> permissionsToRequest = new ArrayList<>();
173
+
174
+ // Add basic Bluetooth permissions
175
+ permissionsToRequest.add(Manifest.permission.BLUETOOTH);
176
+ permissionsToRequest.add(Manifest.permission.BLUETOOTH_ADMIN);
177
+ permissionsToRequest.add(Manifest.permission.ACCESS_FINE_LOCATION);
178
+ permissionsToRequest.add(Manifest.permission.ACCESS_COARSE_LOCATION);
179
+
180
+ // Add Android 12+ Bluetooth permissions
181
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
182
+ permissionsToRequest.add(Manifest.permission.BLUETOOTH_CONNECT);
183
+ permissionsToRequest.add(Manifest.permission.BLUETOOTH_SCAN);
184
+ }
185
+
186
+ requestPermissionForAliases(permissionsToRequest.toArray(new String[0]), call, "permissionsCallback");
187
+ }
188
+
189
+ @PluginMethod
190
+ public void echo(PluginCall call) {
191
+ String value = call.getString("value");
192
+ if (value == null)
193
+ value = "";
194
+ JSObject result = new JSObject();
195
+ result.put("value", value);
196
+ call.resolve(result);
197
+ }
198
+
199
+ @PluginMethod
200
+ public void initialize(PluginCall call) {
201
+ Log.d(TAG, "🔧 Initializing Zebra printer plugin...");
202
+
203
+ // Initialize Bluetooth adapter if not already initialized
204
+ if (bluetoothAdapter == null) {
205
+ bluetoothManager = (BluetoothManager) getContext().getSystemService(Context.BLUETOOTH_SERVICE);
206
+ bluetoothAdapter = bluetoothManager.getAdapter();
207
+
208
+ if (bluetoothAdapter != null) {
209
+ bluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner();
210
+ Log.d(TAG, "✅ Bluetooth adapter initialized");
211
+ } else {
212
+ Log.e(TAG, "❌ Bluetooth not supported");
213
+ call.reject("Initialization failed", "Bluetooth not supported");
214
+ return;
215
+ }
216
+ }
217
+
218
+ JSObject result = new JSObject();
219
+ result.put("success", true);
220
+ result.put("message", "Zebra printer plugin initialized");
221
+ result.put("bluetoothSupported", bluetoothAdapter != null);
222
+ result.put("bluetoothEnabled", bluetoothAdapter != null && bluetoothAdapter.isEnabled());
223
+ call.resolve(result);
224
+ }
225
+
226
+ @PluginMethod
227
+ public void sendRawCommand(PluginCall call) {
228
+ String command = call.getString("command");
229
+ if (command == null || command.isEmpty()) {
230
+ call.reject("Invalid command", "Command cannot be empty");
231
+ return;
232
+ }
233
+
234
+ Log.d(TAG, "📤 Sending RAW command: " + command);
235
+
236
+ // Check printer status first
237
+ if (!isConnected) {
238
+ Log.e(TAG, "❌ Not connected to printer");
239
+ call.reject("Send failed", "Not connected to printer");
240
+ return;
241
+ }
242
+
243
+ // Check if we have either BLE characteristic or SPP connection
244
+ if (!useSPP && printerCharacteristic == null) {
245
+ Log.e(TAG, "❌ No BLE characteristic available");
246
+ call.reject("Send failed", "No BLE characteristic available");
247
+ return;
248
+ }
249
+
250
+ // CRITICAL FIX: Store call reference for async completion
251
+ currentPrintCall = call;
252
+
253
+ // Send raw command directly to printer using async chunking
254
+ if (sendDataToPrinter(command)) {
255
+ Log.d(TAG, "✅ Started sending RAW command (async)");
256
+ // Don't resolve here - wait for sendNextChunkAsync to complete
257
+ } else {
258
+ Log.e(TAG, "❌ Failed to start sending RAW command");
259
+ currentPrintCall = null;
260
+ call.reject("Send failed", "Failed to start sending RAW command to printer");
261
+ }
262
+ }
263
+
264
+ @PluginMethod
265
+ public void connectToPrinter(PluginCall call) {
266
+ Log.d(TAG, "🔌 ConnectToPrinter method called");
267
+
268
+ String name = call.getString("name");
269
+ String address = call.getString("address");
270
+ String type = call.getString("type", "bluetooth");
271
+
272
+ Log.d(TAG, "📱 Connection request - Name: " + (name != null ? name : "none") +
273
+ ", Address: " + (address != null ? address : "none") +
274
+ ", Type: " + type);
275
+
276
+ // For Android, we only support Bluetooth connections
277
+ if (!"bluetooth".equals(type)) {
278
+ call.reject("Unsupported connection type", "Android only supports Bluetooth connections");
279
+ return;
280
+ }
281
+
282
+ // If address is provided, try to connect to specific device
283
+ if (address != null && !address.isEmpty()) {
284
+ // Find device with matching address
285
+ BluetoothDevice targetDevice = null;
286
+ for (BluetoothDevice device : discoveredDevices) {
287
+ if (address.equals(device.getAddress())) {
288
+ targetDevice = device;
289
+ break;
290
+ }
291
+ }
292
+
293
+ // If not found in discovered devices, try to get from paired devices
294
+ if (targetDevice == null && bluetoothAdapter != null) {
295
+ if (ActivityCompat.checkSelfPermission(getContext(),
296
+ Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED) {
297
+ for (BluetoothDevice device : bluetoothAdapter.getBondedDevices()) {
298
+ if (address.equals(device.getAddress())) {
299
+ targetDevice = device;
300
+ discoveredDevices.add(device);
301
+ break;
302
+ }
303
+ }
304
+ }
305
+ }
306
+
307
+ if (targetDevice != null) {
308
+ Log.d(TAG, "🔌 Connecting to specific printer: " + targetDevice.getName());
309
+ connectToDevice(targetDevice);
310
+ JSObject result = new JSObject();
311
+ result.put("success", true);
312
+ result.put("connected", false);
313
+ result.put("name", targetDevice.getName());
314
+ result.put("address", address);
315
+ result.put("type", "bluetooth");
316
+ result.put("message", "Connecting to printer...");
317
+ call.resolve(result);
318
+ return;
319
+ } else {
320
+ call.reject("Printer not found", "Printer with address " + address + " not found");
321
+ return;
322
+ }
323
+ }
324
+
325
+ // If no address provided, try to connect to first available printer
326
+ if (!discoveredDevices.isEmpty()) {
327
+ BluetoothDevice firstPrinter = discoveredDevices.get(0);
328
+ Log.d(TAG, "🔌 Connecting to first available printer: " + firstPrinter.getName());
329
+ connectToDevice(firstPrinter);
330
+ JSObject result = new JSObject();
331
+ result.put("success", true);
332
+ result.put("connected", false);
333
+ result.put("name", firstPrinter.getName());
334
+ result.put("address", firstPrinter.getAddress());
335
+ result.put("type", "bluetooth");
336
+ result.put("message", "Connecting to printer...");
337
+ call.resolve(result);
338
+ } else {
339
+ Log.e(TAG, "❌ No printers discovered");
340
+ call.reject("No printers found", "No Zebra printers discovered. Please scan for printers first.");
341
+ }
342
+ }
343
+
344
+ @PluginMethod
345
+ public void printText(PluginCall call) {
346
+ String text = call.getString("text");
347
+ if (text == null)
348
+ text = "";
349
+ Integer fontSize = call.getInt("fontSize");
350
+ Boolean bold = call.getBoolean("bold");
351
+ String alignment = call.getString("alignment");
352
+
353
+ Log.d(TAG, "🖨️ Printing text: " + text);
354
+
355
+ // CRITICAL FIX: Check connection status and verify socket is still connected
356
+ if (!isConnected) {
357
+ Log.e(TAG, "❌ Not connected to printer");
358
+ call.reject("Print failed", "Not connected to printer");
359
+ return;
360
+ }
361
+
362
+ // CRITICAL FIX: Verify SPP socket is still connected
363
+ // But don't disconnect immediately - try to reconnect first
364
+ if (useSPP && sppSocket != null) {
365
+ try {
366
+ if (!sppSocket.isConnected()) {
367
+ Log.w(TAG, "⚠️ SPP socket is not connected, will try to reconnect");
368
+ // CRITICAL FIX: Don't disconnect immediately - try to reconnect first
369
+ // Keep connection status until reconnection attempt completes
370
+
371
+ // Try to reconnect if we have device address
372
+ if (connectedPrinterAddress != null && bluetoothAdapter != null) {
373
+ Log.d(TAG, "🔄 Attempting to reconnect to: " + connectedPrinterAddress);
374
+ try {
375
+ BluetoothDevice device = bluetoothAdapter.getRemoteDevice(connectedPrinterAddress);
376
+ if (device != null) {
377
+ // CRITICAL FIX: Reconnect synchronously to ensure it completes before continuing
378
+ // Don't start background thread - reconnect directly
379
+ boolean reconnected = connectSPP(device);
380
+
381
+ if (reconnected && sppSocket != null && sppSocket.isConnected()) {
382
+ Log.d(TAG, "✅ Socket reconnected, continuing with print");
383
+ // CRITICAL FIX: Ensure isConnected is set after successful reconnection
384
+ if (!isConnected) {
385
+ isConnected = true;
386
+ useSPP = true;
387
+ Log.d(TAG, "✅ Connection status updated after reconnection");
388
+ }
389
+ // Continue with print - don't reject
390
+ } else {
391
+ Log.w(TAG, "⚠️ Reconnection failed or socket still not connected");
392
+ // CRITICAL FIX: Only disconnect if reconnection definitively failed
393
+ // Check if socket is actually invalid before disconnecting
394
+ boolean socketInvalid = true;
395
+ if (sppSocket != null) {
396
+ try {
397
+ if (sppSocket.isConnected()) {
398
+ // Socket is connected, keep connection alive
399
+ socketInvalid = false;
400
+ if (!isConnected) {
401
+ isConnected = true;
402
+ useSPP = true;
403
+ Log.d(TAG, "✅ Connection status updated - socket is valid");
404
+ }
405
+ } else {
406
+ // Try to verify socket is actually invalid
407
+ try {
408
+ sppSocket.getRemoteDevice();
409
+ // If we can get remote device, socket might still be valid
410
+ socketInvalid = false;
411
+ if (!isConnected) {
412
+ isConnected = true;
413
+ useSPP = true;
414
+ Log.d(TAG, "✅ Connection status updated - socket might be valid");
415
+ }
416
+ } catch (Exception e) {
417
+ // Socket is definitely invalid
418
+ socketInvalid = true;
419
+ }
420
+ }
421
+ } catch (Exception e) {
422
+ // Error checking socket status, assume invalid
423
+ Log.e(TAG, "❌ Error checking socket status: " + e.getMessage());
424
+ socketInvalid = true;
425
+ }
426
+ }
427
+
428
+ if (socketInvalid) {
429
+ // Close invalid socket only if reconnection didn't work
430
+ if (sppSocket != null) {
431
+ try {
432
+ sppSocket.close();
433
+ } catch (IOException closeException) {
434
+ Log.e(TAG, "❌ Error closing invalid socket", closeException);
435
+ }
436
+ sppSocket = null;
437
+ }
438
+
439
+ useSPP = false;
440
+ isConnected = false;
441
+
442
+ call.reject("Print failed",
443
+ "SPP socket is not connected. Reconnection failed.");
444
+ return;
445
+ } else {
446
+ // Socket is valid, continue with print
447
+ Log.d(TAG, "✅ Socket is valid, continuing with print");
448
+ }
449
+ }
450
+ } else {
451
+ // No device found, disconnect
452
+ isConnected = false;
453
+ useSPP = false;
454
+ if (sppSocket != null) {
455
+ try {
456
+ sppSocket.close();
457
+ } catch (IOException closeException) {
458
+ Log.e(TAG, "❌ Error closing invalid socket", closeException);
459
+ }
460
+ sppSocket = null;
461
+ sppOutputStream = null;
462
+ }
463
+ call.reject("Print failed", "SPP socket is not connected and device not found");
464
+ return;
465
+ }
466
+ } catch (Exception reconnectException) {
467
+ Log.e(TAG, "❌ Error during reconnection attempt: " + reconnectException.getMessage());
468
+ // Disconnect only if reconnection definitively failed
469
+ isConnected = false;
470
+ useSPP = false;
471
+ if (sppSocket != null) {
472
+ try {
473
+ sppSocket.close();
474
+ } catch (IOException closeException) {
475
+ Log.e(TAG, "❌ Error closing invalid socket", closeException);
476
+ }
477
+ sppSocket = null;
478
+ sppOutputStream = null;
479
+ }
480
+ call.reject("Print failed", "SPP socket is not connected. Reconnection failed.");
481
+ return;
482
+ }
483
+ } else {
484
+ // No address or adapter, disconnect
485
+ isConnected = false;
486
+ useSPP = false;
487
+ if (sppSocket != null) {
488
+ try {
489
+ sppSocket.close();
490
+ } catch (IOException closeException) {
491
+ Log.e(TAG, "❌ Error closing invalid socket", closeException);
492
+ }
493
+ sppSocket = null;
494
+ sppOutputStream = null;
495
+ }
496
+ call.reject("Print failed", "SPP socket is not connected and no device address available");
497
+ return;
498
+ }
499
+ }
500
+ } catch (Exception e) {
501
+ Log.e(TAG, "❌ Error checking SPP socket connection: " + e.getMessage());
502
+ // Don't disconnect on check error - socket might still be valid
503
+ // Only disconnect if we're sure socket is invalid
504
+ Log.w(TAG, "⚠️ Socket check failed, but keeping connection alive");
505
+ }
506
+ }
507
+
508
+ // Check if we have either BLE characteristic or SPP connection
509
+ if (!useSPP && printerCharacteristic == null) {
510
+ Log.e(TAG, "❌ No BLE characteristic available");
511
+ call.reject("Print failed", "No BLE characteristic available");
512
+ return;
513
+ }
514
+
515
+ // CRITICAL FIX: Check if it's already a ZPL command
516
+ String dataToSend;
517
+ if (text.startsWith("^XA") || text.startsWith("~") || text.startsWith("!")) {
518
+ // It's already a ZPL/CPCL command - send it RAW
519
+ Log.d(TAG, "📤 Sending RAW ZPL/CPCL command...");
520
+ dataToSend = text;
521
+ } else {
522
+ // It's plain text - create ZPL wrapper with proper formatting
523
+ Log.d(TAG, "📤 Converting plain text to ZPL...");
524
+ int x = "center".equals(alignment) ? 200 : ("right".equals(alignment) ? 350 : 50);
525
+ String fontStyle = (bold != null && bold) ? "B" : "N";
526
+ int size = (fontSize != null) ? fontSize : 12;
527
+ // Improved ZPL command with proper line breaks and formatting
528
+ dataToSend = "^XA\n^FO" + x + ",50^A0" + fontStyle + "," + size + "," + size + "^FD" + text + "^FS\n^XZ";
529
+ }
530
+
531
+ Log.d(TAG, "📦 Final data to send: " + dataToSend);
532
+
533
+ // CRITICAL FIX: Store call reference for async completion
534
+ currentPrintCall = call;
535
+
536
+ // Send to printer using async chunking system
537
+ if (sendDataToPrinter(dataToSend)) {
538
+ Log.d(TAG, "✅ Started sending data (async)");
539
+ // Don't resolve here - wait for sendNextChunkAsync to complete
540
+ } else {
541
+ Log.e(TAG, "❌ Failed to start sending data");
542
+ currentPrintCall = null;
543
+ call.reject("Print failed", "Failed to start sending data to printer");
544
+ }
545
+ }
546
+
547
+ // ASYNC: Async sendDataToPrinter with proper callback handling
548
+ private boolean sendDataToPrinter(String data) {
549
+ if (!isConnected) {
550
+ Log.e(TAG, "❌ Not connected to printer");
551
+ return false;
552
+ }
553
+
554
+ // Use SPP if available (more reliable for printing)
555
+ if (useSPP && sppOutputStream != null) {
556
+ Log.d(TAG, "📤 Using SPP for data transmission");
557
+ return sendDataToPrinterSPP(data);
558
+ }
559
+
560
+ // Fallback to BLE with async chunking
561
+ if (printerCharacteristic == null) {
562
+ Log.e(TAG, "❌ No BLE characteristic available");
563
+ return false;
564
+ }
565
+
566
+ try {
567
+ currentData = data.getBytes("UTF-8");
568
+ currentOffset = 0;
569
+ isWriting = false;
570
+
571
+ Log.d(TAG, "📏 Total data size: " + currentData.length + " bytes");
572
+ Log.d(TAG, "📏 MTU size: " + BLE_MTU_SIZE + " bytes");
573
+
574
+ // Start async chunking
575
+ sendNextChunkAsync();
576
+ return true;
577
+
578
+ } catch (Exception e) {
579
+ Log.e(TAG, "❌ Error preparing data for printer", e);
580
+ return false;
581
+ }
582
+ }
583
+
584
+ // ASYNC: Send next chunk with proper async handling
585
+ private void sendNextChunkAsync() {
586
+ if (currentOffset >= currentData.length) {
587
+ // All chunks sent successfully
588
+ Log.d(TAG, "✅ All data sent successfully");
589
+ Log.d(TAG, "📋 Data length: " + currentData.length + " bytes");
590
+
591
+ // Resolve the call
592
+ if (currentPrintCall != null) {
593
+ JSObject result = new JSObject();
594
+ result.put("success", true);
595
+ result.put("message", "Data sent to printer successfully");
596
+ result.put("bytes", currentData.length);
597
+ result.put("processed", true);
598
+ currentPrintCall.resolve(result);
599
+ currentPrintCall = null;
600
+ }
601
+ return;
602
+ }
603
+
604
+ if (isWriting) {
605
+ Log.d(TAG, "⏳ Still writing previous chunk, waiting...");
606
+ // Retry after delay
607
+ new Handler(Looper.getMainLooper()).postDelayed(() -> {
608
+ sendNextChunkAsync();
609
+ }, 50);
610
+ return;
611
+ }
612
+
613
+ int chunkSize = Math.min(BLE_MTU_SIZE, currentData.length - currentOffset);
614
+ byte[] chunk = new byte[chunkSize];
615
+ System.arraycopy(currentData, currentOffset, chunk, 0, chunkSize);
616
+
617
+ Log.d(TAG, "📤 Sending chunk at offset " + currentOffset + ": " + chunkSize + " bytes");
618
+
619
+ printerCharacteristic.setValue(chunk);
620
+ printerCharacteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
621
+
622
+ isWriting = true;
623
+ boolean writeResult = bluetoothGatt.writeCharacteristic(printerCharacteristic);
624
+ Log.d(TAG, "📤 Write result: " + writeResult);
625
+
626
+ if (!writeResult) {
627
+ Log.e(TAG, "❌ Failed to write characteristic at offset " + currentOffset);
628
+ isWriting = false;
629
+
630
+ // Retry after delay
631
+ new Handler(Looper.getMainLooper()).postDelayed(() -> {
632
+ Log.d(TAG, "🔄 Retrying chunk at offset " + currentOffset);
633
+ sendNextChunkAsync();
634
+ }, 100);
635
+ }
636
+ // If writeResult is true, wait for onCharacteristicWrite callback
637
+ }
638
+
639
+ @PluginMethod
640
+ public void getStatus(PluginCall call) {
641
+ JSObject result = new JSObject();
642
+ result.put("connected", isConnected);
643
+ result.put("status", isConnected ? "connected" : "disconnected");
644
+ if (isConnected) {
645
+ result.put("printerAddress", connectedPrinterAddress);
646
+ result.put("printerType", useSPP ? "spp" : "ble");
647
+ result.put("connectionMethod", useSPP ? "SPP (Classic Bluetooth)" : "BLE (Bluetooth Low Energy)");
648
+ }
649
+ call.resolve(result);
650
+ }
651
+
652
+ // NEW: Check printer status with ZPL command
653
+ @PluginMethod
654
+ public void checkPrinterStatus(PluginCall call) {
655
+ if (!isConnected) {
656
+ call.reject("Status check failed", "Not connected to printer");
657
+ return;
658
+ }
659
+
660
+ // Check if we have either BLE characteristic or SPP connection
661
+ if (!useSPP && printerCharacteristic == null) {
662
+ call.reject("Status check failed", "No BLE characteristic available");
663
+ return;
664
+ }
665
+
666
+ // Send printer status command
667
+ String statusCommand = "~HS"; // Zebra printer status command
668
+ Log.d(TAG, "🔍 Sending status command: " + statusCommand);
669
+
670
+ if (sendDataToPrinter(statusCommand)) {
671
+ JSObject result = new JSObject();
672
+ result.put("success", true);
673
+ result.put("message", "Status command sent");
674
+ result.put("command", statusCommand);
675
+ result.put("connectionType", useSPP ? "SPP" : "BLE");
676
+ call.resolve(result);
677
+ } else {
678
+ call.reject("Status check failed", "Failed to send status command");
679
+ }
680
+ }
681
+
682
+ @PluginMethod
683
+ public void scanForPrinters(PluginCall call) {
684
+ Log.d(TAG, "🔍 Starting printer scan...");
685
+
686
+ // Check permissions first
687
+ if (!checkBluetoothPermissions()) {
688
+ Log.e(TAG, "❌ Missing Bluetooth permissions");
689
+ JSObject result = new JSObject();
690
+ result.put("success", false);
691
+ result.put("error", "Missing Bluetooth permissions. Please request permissions first.");
692
+ call.resolve(result);
693
+ return;
694
+ }
695
+
696
+ if (bluetoothAdapter == null) {
697
+ Log.e(TAG, "❌ Bluetooth not supported");
698
+ JSObject result = new JSObject();
699
+ result.put("success", false);
700
+ result.put("error", "Bluetooth not supported");
701
+ call.resolve(result);
702
+ return;
703
+ }
704
+
705
+ if (!bluetoothAdapter.isEnabled()) {
706
+ Log.e(TAG, "❌ Bluetooth is disabled");
707
+ JSObject result = new JSObject();
708
+ result.put("success", false);
709
+ result.put("error", "Bluetooth is disabled");
710
+ call.resolve(result);
711
+ return;
712
+ }
713
+
714
+ // Start scanning (Classic Bluetooth for Zebra printers)
715
+ startScanning();
716
+
717
+ // Wait a bit for scanning to find devices, then return results
718
+ new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
719
+ @Override
720
+ public void run() {
721
+ List<JSObject> printers = new ArrayList<>();
722
+ for (BluetoothDevice device : discoveredDevices) {
723
+ String deviceName = device.getName();
724
+ if (deviceName != null && (deviceName.toLowerCase().contains("zebra") ||
725
+ deviceName.toLowerCase().contains("zt") ||
726
+ deviceName.toLowerCase().contains("xxzhn") ||
727
+ deviceName.toLowerCase().contains("printer") ||
728
+ deviceName.toLowerCase().contains("label"))) {
729
+ JSObject printer = new JSObject();
730
+ printer.put("name", deviceName);
731
+ printer.put("address", device.getAddress());
732
+ printer.put("type", "bluetooth");
733
+ printer.put("paired", device.getBondState() == BluetoothDevice.BOND_BONDED);
734
+ printers.add(printer);
735
+ }
736
+ }
737
+
738
+ JSObject result = new JSObject();
739
+ result.put("success", true);
740
+ result.put("printers", printers.toArray(new JSObject[0]));
741
+ result.put("count", printers.size());
742
+ result.put("message", "Found " + printers.size() + " printer(s)");
743
+ call.resolve(result);
744
+ }
745
+ }, 3000); // Wait 3 seconds for scanning
746
+ }
747
+
748
+ private void startScanning() {
749
+ if (isScanning) {
750
+ Log.d(TAG, "🔍 Already scanning...");
751
+ return;
752
+ }
753
+
754
+ Log.d(TAG, "🔍 Starting Classic Bluetooth scan (for Zebra printers)...");
755
+ isScanning = true;
756
+
757
+ // Clear previous discoveries
758
+ discoveredDevices.clear();
759
+
760
+ // Start Classic Bluetooth scan (Zebra printers use Classic Bluetooth SPP)
761
+ if (bluetoothAdapter.isDiscovering()) {
762
+ bluetoothAdapter.cancelDiscovery();
763
+ }
764
+
765
+ bluetoothAdapter.startDiscovery();
766
+ Log.d(TAG, "🔍 Classic Bluetooth scan started for Zebra printers");
767
+
768
+ // Also try BLE scan as fallback (but don't auto-connect)
769
+ if (bluetoothLeScanner != null) {
770
+ bluetoothLeScanner.startScan(new ScanCallback() {
771
+ @Override
772
+ public void onScanResult(int callbackType, ScanResult result) {
773
+ BluetoothDevice device = result.getDevice();
774
+ String deviceName = device.getName();
775
+
776
+ Log.d(TAG, "📱 Found BLE device: " + (deviceName != null ? deviceName : "Unknown"));
777
+
778
+ // Save BLE Zebra printer but don't auto-connect
779
+ if (deviceName != null && (deviceName.toLowerCase().contains("zebra") ||
780
+ deviceName.toLowerCase().contains("zt") ||
781
+ deviceName.toLowerCase().contains("xxzhn"))) {
782
+ Log.d(TAG, "🖨️ Found Zebra printer via BLE: " + deviceName);
783
+
784
+ // Save device if not already discovered
785
+ if (!discoveredDevices.contains(device)) {
786
+ discoveredDevices.add(device);
787
+ }
788
+ }
789
+ }
790
+
791
+ @Override
792
+ public void onScanFailed(int errorCode) {
793
+ Log.e(TAG, "❌ BLE scan failed with error: " + errorCode);
794
+ }
795
+ });
796
+ }
797
+
798
+ // Stop scanning after 15 seconds (Classic Bluetooth needs more time)
799
+ new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
800
+
801
+ @Override
802
+ public void run() {
803
+ if (isScanning) {
804
+ Log.d(TAG, "⏹️ Stopping scan after timeout");
805
+ if (bluetoothLeScanner != null) {
806
+ bluetoothLeScanner.stopScan(new ScanCallback() {
807
+ });
808
+ }
809
+ if (bluetoothAdapter.isDiscovering()) {
810
+ bluetoothAdapter.cancelDiscovery();
811
+ }
812
+ isScanning = false;
813
+ }
814
+ }},15000);}
815
+
816
+ // Classic Bluetooth discovery callback
817
+ private final BroadcastReceiver bluetoothReceiver=new BroadcastReceiver(){@Override public void onReceive(Context context,Intent intent){String action=intent.getAction();Log.d(TAG,"📡 Bluetooth receiver action: "+action);
818
+
819
+ if(BluetoothDevice.ACTION_FOUND.equals(action)){BluetoothDevice device=intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);String deviceName=device.getName();
820
+
821
+ Log.d(TAG,"📱 Found Classic Bluetooth device: "+(deviceName!=null?deviceName:"Unknown")+" ("+device.getAddress()+")");Log.d(TAG,"📱 Device bond state: "+device.getBondState());Log.d(TAG,"📱 Device type: "+device.getType());
822
+
823
+ // Check if it's a Zebra printer (expanded filter)
824
+ if(deviceName!=null&&(deviceName.toLowerCase().contains("zebra")||deviceName.toLowerCase().contains("zt")||deviceName.toLowerCase().contains("xxzhn")||deviceName.toLowerCase().contains("printer")||deviceName.toLowerCase().contains("label")||deviceName.toLowerCase().contains("zebraprinter")||deviceName.toLowerCase().contains("zebra_printer"))){Log.d(TAG,"🖨️ Found Zebra printer via Classic Bluetooth: "+deviceName);
825
+
826
+ // Save device if not already discovered
827
+ if(!discoveredDevices.contains(device)){discoveredDevices.add(device);}
828
+
829
+ // Auto-connect if not already connected
830
+ // CRITICAL FIX: Always try to connect, but connectToDevice will check if
831
+ // already connected
832
+ Log.d(TAG,"🔌 Auto-connecting to printer via Classic Bluetooth...");connectToDevice(device);}}else if(BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action)){BluetoothDevice device=intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);int bondState=intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,BluetoothDevice.BOND_NONE);Log.d(TAG,"🔗 Bond state changed for "+device.getName()+": "+bondState);
833
+
834
+ if(bondState==BluetoothDevice.BOND_BONDED){Log.d(TAG,"✅ Device bonded successfully: "+device.getName());
835
+ // Try to connect via SPP after bonding
836
+ // CRITICAL FIX: Check if already connected to this device
837
+ if((!isConnected||connectedPrinterAddress==null||!connectedPrinterAddress.equals(device.getAddress()))&&discoveredDevices.contains(device)){Log.d(TAG,"🔌 Attempting SPP connection after bonding...");new Thread(()->{if(connectSPP(device)){Log.d(TAG,"✅ Connected via SPP after bonding");
838
+ // Ensure isConnected is set (it should be set in connectSPP, but double-check)
839
+ if(!isConnected){isConnected=true;connectedPrinterAddress=device.getAddress();useSPP=true;Log.d(TAG,"✅ Connection status updated after bonding");}}else{Log.e(TAG,"❌ Failed to connect via SPP after bonding");}}).start();}else{Log.d(TAG,"✅ Already connected to this printer, skipping connection after bonding");}}else if(bondState==BluetoothDevice.BOND_NONE){Log.d(TAG,"❌ Device bonding failed: "+device.getName());}}}};
840
+
841
+ @PluginMethod
842
+ public void connect(PluginCall call) {
843
+ Log.d(TAG, "🔌 Connect method called");
844
+
845
+ String address = call.getString("address");
846
+ String type = call.getString("type", "bluetooth");
847
+
848
+ Log.d(TAG, "📱 Connection request - Address: " + (address != null ? address : "none") + ", Type: " + type);
849
+
850
+ // For Android, we only support Bluetooth connections
851
+ if (!"bluetooth".equals(type)) {
852
+ call.reject("Unsupported connection type", "Android only supports Bluetooth connections");
853
+ return;
854
+ }
855
+
856
+ // If address is provided, try to connect to specific device
857
+ if (address != null && !address.isEmpty()) {
858
+ // Find device with matching address
859
+ BluetoothDevice targetDevice = null;
860
+ for (BluetoothDevice device : discoveredDevices) {
861
+ if (address.equals(device.getAddress())) {
862
+ targetDevice = device;
863
+ break;
864
+ }
865
+ }
866
+
867
+ if (targetDevice != null) {
868
+ Log.d(TAG, "🔌 Connecting to specific printer: " + targetDevice.getName());
869
+ connectToDevice(targetDevice);
870
+ JSObject result = new JSObject();
871
+ result.put("success", true);
872
+ result.put("connected", false);
873
+ result.put("address", address);
874
+ result.put("type", "bluetooth");
875
+ result.put("message", "Connecting to printer...");
876
+ call.resolve(result);
877
+ return;
878
+ } else {
879
+ call.reject("Printer not found", "Printer with address " + address + " not found");
880
+ return;
881
+ }
882
+ }
883
+
884
+ // If no address provided, try to connect to first available printer
885
+ if (!discoveredDevices.isEmpty()) {
886
+ BluetoothDevice firstPrinter = discoveredDevices.get(0);
887
+ Log.d(TAG, "🔌 Connecting to first available printer: " + firstPrinter.getName());
888
+ connectToDevice(firstPrinter);
889
+ JSObject result = new JSObject();
890
+ result.put("success", true);
891
+ result.put("connected", false);
892
+ result.put("address", firstPrinter.getAddress());
893
+ result.put("type", "bluetooth");
894
+ result.put("message", "Connecting to printer...");
895
+ call.resolve(result);
896
+ } else {
897
+ Log.e(TAG, "❌ No printers discovered");
898
+ call.reject("No printers found", "No Zebra printers discovered. Please scan for printers first.");
899
+ }
900
+ }
901
+
902
+ private boolean checkBluetoothPermissions() {
903
+ // Check basic Bluetooth permissions
904
+ if (ContextCompat.checkSelfPermission(getContext(),
905
+ Manifest.permission.BLUETOOTH) != PackageManager.PERMISSION_GRANTED) {
906
+ return false;
907
+ }
908
+
909
+ if (ContextCompat.checkSelfPermission(getContext(),
910
+ Manifest.permission.BLUETOOTH_ADMIN) != PackageManager.PERMISSION_GRANTED) {
911
+ return false;
912
+ }
913
+
914
+ // Check location permissions (required for BLE scanning)
915
+ if (ContextCompat.checkSelfPermission(getContext(),
916
+ Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
917
+ return false;
918
+ }
919
+
920
+ // Check Android 12+ Bluetooth permissions
921
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
922
+ if (ContextCompat.checkSelfPermission(getContext(),
923
+ Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
924
+ return false;
925
+ }
926
+
927
+ if (ContextCompat.checkSelfPermission(getContext(),
928
+ Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) {
929
+ return false;
930
+ }
931
+ }
932
+
933
+ return true;
934
+ }
935
+
936
+ private void connectToDevice(BluetoothDevice device) {
937
+ Log.d(TAG, "Connecting to device: " + device.getName());
938
+
939
+ // CRITICAL FIX: Check if already connected to this device
940
+ // But don't skip if connection is not valid - always verify socket is actually
941
+ // connected
942
+ if (isConnected && useSPP && sppSocket != null && connectedPrinterAddress != null) {
943
+ if (connectedPrinterAddress.equals(device.getAddress())) {
944
+ try {
945
+ // CRITICAL FIX: Always verify socket is actually connected, not just check
946
+ // isConnected flag
947
+ boolean socketActuallyConnected = sppSocket.isConnected();
948
+ if (socketActuallyConnected && sppOutputStream != null) {
949
+ // Double-check by trying to get remote device
950
+ try {
951
+ sppSocket.getRemoteDevice();
952
+ Log.d(TAG, "✅ Already connected to this device, socket is valid");
953
+ // Verify connection is still valid by checking socket
954
+ return; // Already connected to this device, no need to reconnect
955
+ } catch (Exception e) {
956
+ Log.w(TAG, "⚠️ Socket check failed, will reconnect: " + e.getMessage());
957
+ // Socket check failed, need to reconnect
958
+ isConnected = false;
959
+ useSPP = false;
960
+ if (sppSocket != null) {
961
+ try {
962
+ sppSocket.close();
963
+ } catch (IOException closeException) {
964
+ Log.e(TAG, "Error closing invalid socket", closeException);
965
+ }
966
+ }
967
+ sppSocket = null;
968
+ sppOutputStream = null;
969
+ }
970
+ } else {
971
+ Log.w(TAG, "⚠️ Socket exists but not connected (isConnected=" + socketActuallyConnected
972
+ + "), will reconnect");
973
+ // Socket exists but not connected, need to reconnect
974
+ isConnected = false;
975
+ useSPP = false;
976
+ if (sppSocket != null) {
977
+ try {
978
+ sppSocket.close();
979
+ } catch (IOException e) {
980
+ Log.e(TAG, "❌ Error closing invalid socket", e);
981
+ }
982
+ }
983
+ sppSocket = null;
984
+ sppOutputStream = null;
985
+ }
986
+ } catch (Exception e) {
987
+ Log.w(TAG, "⚠️ Error checking existing connection: " + e.getMessage());
988
+ // Connection check failed, need to reconnect
989
+ isConnected = false;
990
+ useSPP = false;
991
+ if (sppSocket != null) {
992
+ try {
993
+ sppSocket.close();
994
+ } catch (IOException closeException) {
995
+ Log.e(TAG, "❌ Error closing invalid socket", closeException);
996
+ }
997
+ }
998
+ sppSocket = null;
999
+ sppOutputStream = null;
1000
+ }
1001
+ } else {
1002
+ Log.d(TAG, "🔌 Connected to different device, will reconnect to new device");
1003
+ // Connected to different device, need to disconnect first
1004
+ }
1005
+ } else {
1006
+ // CRITICAL FIX: If isConnected is true but socket is null, reset isConnected
1007
+ if (isConnected && (sppSocket == null || !useSPP)) {
1008
+ Log.w(TAG, "⚠️ isConnected is true but socket is null or useSPP is false, resetting connection status");
1009
+ isConnected = false;
1010
+ useSPP = false;
1011
+ connectedPrinterAddress = null;
1012
+ }
1013
+ }
1014
+
1015
+ // Disconnect from current device if connected to different device
1016
+ if (bluetoothGatt != null) {
1017
+ bluetoothGatt.disconnect();
1018
+ bluetoothGatt.close();
1019
+ }
1020
+
1021
+ // Close SPP connection if exists and connected to different device
1022
+ if (sppSocket != null
1023
+ && (connectedPrinterAddress == null || !connectedPrinterAddress.equals(device.getAddress()))) {
1024
+ try {
1025
+ sppSocket.close();
1026
+ } catch (IOException e) {
1027
+ Log.e(TAG, "❌ Error closing SPP socket", e);
1028
+ }
1029
+ sppSocket = null;
1030
+ sppOutputStream = null;
1031
+ }
1032
+
1033
+ // Try SPP first (Bluetooth Classic - more reliable for printing)
1034
+ Log.d(TAG, "🔌 Attempting Bluetooth Classic (SPP) connection first...");
1035
+ if (connectSPP(device)) {
1036
+ Log.d(TAG, "✅ Connected via Bluetooth Classic (SPP)");
1037
+ // CRITICAL FIX: Ensure isConnected is set after successful connection
1038
+ if (!isConnected) {
1039
+ Log.w(TAG, "⚠️ Connection successful but isConnected not set, setting now...");
1040
+ isConnected = true;
1041
+ connectedPrinterAddress = device.getAddress();
1042
+ useSPP = true;
1043
+ }
1044
+ Log.d(TAG, "✅ Final connection status in connectToDevice: isConnected=" + isConnected + ", address="
1045
+ + connectedPrinterAddress);
1046
+ return;
1047
+ }
1048
+
1049
+ // CRITICAL: Don't use BLE fallback - only use Classic Bluetooth (SPP)
1050
+ // If SPP fails, check if bonding is in progress
1051
+ Log.d(TAG, "⏳ Bluetooth Classic (SPP) connection pending...");
1052
+ Log.d(TAG, "⚠️ NOT using BLE fallback - Classic Bluetooth (SPP) is required for reliable printing");
1053
+
1054
+ // Check if device is bonding - if so, wait for bonding to complete
1055
+ if (device.getBondState() == BluetoothDevice.BOND_BONDING) {
1056
+ Log.d(TAG, "⏳ Device is bonding, will connect automatically after bonding completes...");
1057
+ // Connection will happen automatically after bonding completes via
1058
+ // BroadcastReceiver
1059
+ return;
1060
+ }
1061
+
1062
+ // If device is not bonded and not bonding, try to bond first
1063
+ if (device.getBondState() != BluetoothDevice.BOND_BONDED) {
1064
+ Log.d(TAG, "🔗 Device not bonded, attempting to pair...");
1065
+ if (ActivityCompat.checkSelfPermission(getContext(),
1066
+ Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED) {
1067
+ boolean bondResult = device.createBond();
1068
+ Log.d(TAG, "🔗 Bonding initiated: " + bondResult);
1069
+ // Connection will happen automatically after bonding completes via
1070
+ // BroadcastReceiver
1071
+ return;
1072
+ } else {
1073
+ Log.e(TAG, "❌ Missing BLUETOOTH_CONNECT permission for bonding");
1074
+ return;
1075
+ }
1076
+ }
1077
+
1078
+ // If we get here, device is bonded but SPP connection failed
1079
+ // Try to connect again after a short delay
1080
+ Log.d(TAG, "🔄 Device is bonded but SPP connection failed, will retry...");
1081
+ new Thread(() -> {
1082
+ try {
1083
+ Thread.sleep(1000); // Wait 1 second (reduced from 2 for faster connection)
1084
+ if (connectSPP(device)) {
1085
+ Log.d(TAG, "✅ Connected via SPP after retry");
1086
+ // Ensure isConnected is set (it should be set in connectSPP, but double-check)
1087
+ if (!isConnected) {
1088
+ isConnected = true;
1089
+ connectedPrinterAddress = device.getAddress();
1090
+ useSPP = true;
1091
+ Log.d(TAG, "✅ Connection status updated after retry");
1092
+ }
1093
+ } else {
1094
+ Log.e(TAG, "❌ SPP connection failed after retry");
1095
+ }
1096
+ } catch (InterruptedException e) {
1097
+ Log.e(TAG, "❌ Retry thread interrupted", e);
1098
+ }
1099
+ }).start();
1100
+ }
1101
+
1102
+ // SPP connection method for Bluetooth Classic - FIXED for ZQ320 (no ANR, stable)
1103
+ private boolean connectSPP(BluetoothDevice device) {
1104
+ // 1. Bonding
1105
+ if (device.getBondState() != BluetoothDevice.BOND_BONDED) {
1106
+ Log.d(TAG, "Not bonded → creating bond (PIN usually 0000)");
1107
+ if (ActivityCompat.checkSelfPermission(getContext(), Manifest.permission.BLUETOOTH_CONNECT)
1108
+ == PackageManager.PERMISSION_GRANTED) {
1109
+ device.createBond();
1110
+ }
1111
+ return false; // Wait for BOND_BONDED via receiver
1112
+ }
1113
+
1114
+ // 2. Clean old socket
1115
+ if (sppSocket != null) {
1116
+ try {
1117
+ sppSocket.close();
1118
+ } catch (Exception ignored) {
1119
+ }
1120
+ sppSocket = null;
1121
+ sppOutputStream = null;
1122
+ }
1123
+
1124
+ // 3. Connect with timeout (runs in background, keeps main thread free)
1125
+ ExecutorService executor = Executors.newSingleThreadExecutor();
1126
+ Future<Boolean> future = executor.submit(() -> {
1127
+ try {
1128
+ sppSocket = device.createRfcommSocketToServiceRecord(SPP_UUID);
1129
+ sppSocket.connect();
1130
+ sppOutputStream = sppSocket.getOutputStream();
1131
+ return true;
1132
+ } catch (Exception e) {
1133
+ Log.e(TAG, "SPP connect failed: " + e.getMessage());
1134
+ return false;
1135
+ }
1136
+ });
1137
+
1138
+ boolean success = false;
1139
+ try {
1140
+ success = future.get(10, TimeUnit.SECONDS); // 10s timeout
1141
+ if (success && sppSocket != null && sppSocket.isConnected()) {
1142
+ isConnected = true;
1143
+ connectedPrinterAddress = device.getAddress();
1144
+ useSPP = true;
1145
+ Log.d(TAG, "ZQ320 CONNECTED VIA SPP! ✅");
1146
+ return true;
1147
+ }
1148
+ } catch (TimeoutException e) {
1149
+ Log.e(TAG, "SPP timeout while connecting: " + e.getMessage());
1150
+ future.cancel(true); // interrupt background connect to avoid races
1151
+ } catch (Exception e) {
1152
+ Log.e(TAG, "SPP connect error: " + e.getMessage());
1153
+ } finally {
1154
+ executor.shutdownNow(); // ensure the task stops before fallback
1155
+ try {
1156
+ executor.awaitTermination(2, TimeUnit.SECONDS);
1157
+ } catch (InterruptedException ie) {
1158
+ Thread.currentThread().interrupt();
1159
+ }
1160
+ }
1161
+
1162
+ // Clean any half-open socket before trying fallback to avoid races
1163
+ if (sppSocket != null) {
1164
+ try {
1165
+ sppSocket.close();
1166
+ } catch (Exception ignored) {
1167
+ }
1168
+ sppSocket = null;
1169
+ sppOutputStream = null;
1170
+ }
1171
+
1172
+ // If connection failed, try fallback UUID (rarely needed)
1173
+ try {
1174
+ Method m = device.getClass().getMethod("createRfcommSocket", new Class[] { int.class });
1175
+ sppSocket = (BluetoothSocket) m.invoke(device, 1);
1176
+ sppSocket.connect();
1177
+ sppOutputStream = sppSocket.getOutputStream();
1178
+ isConnected = true;
1179
+ useSPP = true;
1180
+ connectedPrinterAddress = device.getAddress();
1181
+ Log.d(TAG, "ZQ320 connected via fallback socket! ✅");
1182
+ return true;
1183
+ } catch (Exception e) {
1184
+ Log.e(TAG, "Fallback also failed: " + e.getMessage());
1185
+ }
1186
+
1187
+ return false;
1188
+ }
1189
+
1190
+ // SPP data sending method for Bluetooth Classic
1191
+ private boolean sendDataToPrinterSPP(String data) {
1192
+ // CRITICAL FIX: Check connection status and socket validity
1193
+ if (!isConnected || sppOutputStream == null || sppSocket == null) {
1194
+ Log.e(TAG, "❌ SPP (Bluetooth Classic) not connected");
1195
+ if (currentPrintCall != null) {
1196
+ currentPrintCall.reject("Send failed", "SPP (Bluetooth Classic) not connected");
1197
+ currentPrintCall = null;
1198
+ }
1199
+ return false;
1200
+ }
1201
+
1202
+ // Socket might be temporarily disconnected but still valid
1203
+ boolean socketConnected = false;
1204
+ try {
1205
+ socketConnected = sppSocket.isConnected();
1206
+ if (!socketConnected) {
1207
+ Log.w(TAG, "⚠️ SPP socket is not connected, but will try to send anyway (socket might reconnect)");
1208
+ // Don't disconnect immediately - socket might reconnect
1209
+ // Only mark as disconnected if send actually fails
1210
+ }
1211
+ } catch (Exception e) {
1212
+ Log.w(TAG, "⚠️ Error checking socket connection: " + e.getMessage() + ", will try to send anyway");
1213
+ // Don't disconnect on check error - socket might still be valid
1214
+ // Only disconnect if send actually fails
1215
+ }
1216
+
1217
+ try {
1218
+ byte[] bytes = data.getBytes("UTF-8");
1219
+ Log.d(TAG, "📤 Sending via SPP (Bluetooth Classic): " + bytes.length + " bytes");
1220
+ Log.d(TAG, "📤 Data preview: " + data.substring(0, Math.min(50, data.length())) + "...");
1221
+
1222
+ sppOutputStream.write(bytes);
1223
+ sppOutputStream.flush();
1224
+
1225
+ Log.d(TAG, "✅ SPP (Bluetooth Classic) data sent successfully");
1226
+
1227
+ // CRITICAL FIX: Resolve the call after successful send
1228
+ if (currentPrintCall != null) {
1229
+ JSObject result = new JSObject();
1230
+ result.put("success", true);
1231
+ result.put("message", "Data sent to printer successfully");
1232
+ result.put("bytes", bytes.length);
1233
+ result.put("connectionType", "SPP (Classic Bluetooth)");
1234
+ currentPrintCall.resolve(result);
1235
+ currentPrintCall = null;
1236
+ }
1237
+ return true;
1238
+ } catch (IOException e) {
1239
+ Log.e(TAG, "❌ SPP (Bluetooth Classic) send failed: " + e.getMessage());
1240
+ Log.e(TAG, "❌ Error details: " + e.getClass().getSimpleName());
1241
+
1242
+ // CRITICAL FIX: Don't immediately disconnect - check if socket is actually
1243
+ // closed
1244
+ // IOException can occur even if socket is still valid (e.g., temporary network
1245
+ // issues)
1246
+ boolean socketClosed = false;
1247
+ if (sppSocket != null) {
1248
+ try {
1249
+ if (sppSocket.isConnected()) {
1250
+ Log.d(TAG, "🔍 Socket still connected despite IOException, keeping connection alive");
1251
+ // Socket is still connected, might be temporary issue
1252
+ // Don't disconnect - connection is still valid
1253
+ socketClosed = false;
1254
+ } else {
1255
+ Log.w(TAG, "⚠️ Socket not connected after IOException");
1256
+ // Check if socket is actually closed by trying to read its state
1257
+ try {
1258
+ // Try to check socket state more thoroughly
1259
+ sppSocket.getRemoteDevice();
1260
+ Log.d(TAG, "🔍 Socket might still be valid, keeping connection alive");
1261
+ socketClosed = false;
1262
+ } catch (Exception socketCheckException) {
1263
+ Log.w(TAG, "⚠️ Socket appears to be closed: " + socketCheckException.getMessage());
1264
+ socketClosed = true;
1265
+ }
1266
+ }
1267
+ } catch (Exception checkException) {
1268
+ Log.w(TAG, "⚠️ Error checking socket status: " + checkException.getMessage());
1269
+ // Don't assume socket is closed on check error
1270
+ // Only mark as closed if we're certain
1271
+ try {
1272
+ sppSocket.getRemoteDevice();
1273
+ socketClosed = false;
1274
+ } catch (Exception e2) {
1275
+ socketClosed = true;
1276
+ }
1277
+ }
1278
+ } else {
1279
+ socketClosed = true;
1280
+ }
1281
+
1282
+ // Only disconnect if socket is definitively closed
1283
+ if (socketClosed) {
1284
+ Log.w(TAG, "⚠️ Marking as disconnected due to confirmed socket closure");
1285
+ isConnected = false;
1286
+ useSPP = false;
1287
+
1288
+ // Clean up socket
1289
+ if (sppSocket != null) {
1290
+ try {
1291
+ sppSocket.close();
1292
+ } catch (IOException closeException) {
1293
+ Log.e(TAG, "❌ Error closing socket", closeException);
1294
+ }
1295
+ sppSocket = null;
1296
+ sppOutputStream = null;
1297
+ }
1298
+ } else {
1299
+ Log.d(TAG, "✅ Keeping connection alive - socket still valid despite IOException");
1300
+ }
1301
+
1302
+ // CRITICAL FIX: Reject the call on failure
1303
+ if (currentPrintCall != null) {
1304
+ currentPrintCall.reject("Send failed", "SPP (Bluetooth Classic) send failed: " + e.getMessage());
1305
+ currentPrintCall = null;
1306
+ }
1307
+ return false;
1308
+ }
1309
+ }
1310
+
1311
+ @PluginMethod
1312
+ public void disconnect(PluginCall call) {
1313
+ Log.d(TAG, "🔌 Disconnect method called");
1314
+
1315
+ if (bluetoothGatt != null) {
1316
+ bluetoothGatt.disconnect();
1317
+ bluetoothGatt.close();
1318
+ bluetoothGatt = null;
1319
+ }
1320
+
1321
+ // Disconnect SPP
1322
+ if (sppSocket != null) {
1323
+ try {
1324
+ sppSocket.close();
1325
+ } catch (IOException e) {
1326
+ Log.e(TAG, "❌ Error closing SPP socket", e);
1327
+ }
1328
+ sppSocket = null;
1329
+ sppOutputStream = null;
1330
+ }
1331
+
1332
+ isConnected = false;
1333
+ connectedPrinterAddress = null;
1334
+ printerCharacteristic = null;
1335
+ useSPP = false;
1336
+ isWriting = false;
1337
+
1338
+ Log.d(TAG, "✅ Disconnected from printer");
1339
+
1340
+ JSObject result = new JSObject();
1341
+ result.put("success", true);
1342
+ result.put("connected", false);
1343
+ call.resolve(result);
1344
+ }
1345
+ }