@amitkhare/capacitor-cat-printer 0.5.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.
@@ -0,0 +1,661 @@
1
+ package khare.catprinter.plugin;
2
+
3
+ import android.annotation.SuppressLint;
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.BluetoothGattDescriptor;
10
+ import android.bluetooth.BluetoothGattService;
11
+ import android.bluetooth.BluetoothManager;
12
+ import android.bluetooth.BluetoothProfile;
13
+ import android.bluetooth.le.BluetoothLeScanner;
14
+ import android.bluetooth.le.ScanCallback;
15
+ import android.bluetooth.le.ScanResult;
16
+ import android.bluetooth.le.ScanSettings;
17
+ import android.content.Context;
18
+ import android.graphics.Bitmap;
19
+ import android.graphics.BitmapFactory;
20
+ import android.os.Handler;
21
+ import android.os.Looper;
22
+ import android.util.Base64;
23
+ import android.util.Log;
24
+
25
+ import java.util.ArrayList;
26
+ import java.util.List;
27
+ import java.util.concurrent.ConcurrentLinkedQueue;
28
+ import java.util.concurrent.atomic.AtomicBoolean;
29
+
30
+ /**
31
+ * Core BLE printer functionality.
32
+ * Handles scanning, connection, and printing operations.
33
+ *
34
+ * Note: BLE permissions are handled by CatPrinterPlugin before calling these methods.
35
+ */
36
+ @SuppressLint("MissingPermission")
37
+ public class CatPrinterCore {
38
+ private static final String TAG = "CatPrinterCore";
39
+ private static final int MTU_SIZE = 200;
40
+ private static final int WRITE_DELAY_MS = 20;
41
+
42
+ private final Context context;
43
+ private final Handler mainHandler;
44
+
45
+ private BluetoothAdapter bluetoothAdapter;
46
+ private BluetoothGatt gatt;
47
+ private BluetoothGattCharacteristic txCharacteristic;
48
+
49
+ private String connectedAddress;
50
+ private int paperWidth = PrinterProtocol.WIDTH_58MM;
51
+
52
+ private final AtomicBoolean isScanning = new AtomicBoolean(false);
53
+ private final AtomicBoolean isPaused = new AtomicBoolean(false);
54
+ private final AtomicBoolean isPrinting = new AtomicBoolean(false);
55
+
56
+ private final ConcurrentLinkedQueue<byte[]> writeQueue = new ConcurrentLinkedQueue<>();
57
+ private final AtomicBoolean isWriting = new AtomicBoolean(false);
58
+
59
+ // Callbacks
60
+ private ScanResultCallback scanCallback;
61
+ private ConnectionCallback connectionCallback;
62
+ private PrintCallback printCallback;
63
+
64
+ public interface ScanResultCallback {
65
+ void onDeviceFound(String name, String address, int rssi);
66
+ void onScanComplete(List<BleDeviceInfo> devices);
67
+ void onScanError(String error);
68
+ }
69
+
70
+ public interface ConnectionCallback {
71
+ void onConnected();
72
+ void onDisconnected();
73
+ void onError(String error);
74
+ }
75
+
76
+ public interface PrintCallback {
77
+ void onProgress(int percent, String status, String message);
78
+ void onComplete();
79
+ void onError(String error);
80
+ }
81
+
82
+ public static class BleDeviceInfo {
83
+ public String name;
84
+ public String address;
85
+ public int rssi;
86
+
87
+ public BleDeviceInfo(String name, String address, int rssi) {
88
+ this.name = name;
89
+ this.address = address;
90
+ this.rssi = rssi;
91
+ }
92
+ }
93
+
94
+ public CatPrinterCore(Context context) {
95
+ this.context = context;
96
+ this.mainHandler = new Handler(Looper.getMainLooper());
97
+ initBluetooth();
98
+ }
99
+
100
+ private void initBluetooth() {
101
+ BluetoothManager bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
102
+ if (bluetoothManager != null) {
103
+ bluetoothAdapter = bluetoothManager.getAdapter();
104
+ }
105
+ }
106
+
107
+ private BluetoothLeScanner getScanner() {
108
+ if (bluetoothAdapter == null) {
109
+ initBluetooth();
110
+ }
111
+ if (bluetoothAdapter != null && bluetoothAdapter.isEnabled()) {
112
+ return bluetoothAdapter.getBluetoothLeScanner();
113
+ }
114
+ return null;
115
+ }
116
+
117
+ // ==================== SCANNING ====================
118
+
119
+ private ScanCallback activeScanCallback;
120
+
121
+ public void startScan(int durationMs, ScanResultCallback callback) {
122
+ this.scanCallback = callback;
123
+
124
+ if (bluetoothAdapter == null) {
125
+ initBluetooth();
126
+ }
127
+
128
+ if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) {
129
+ callback.onScanError("Bluetooth is not enabled. Please enable Bluetooth and try again.");
130
+ return;
131
+ }
132
+
133
+ final BluetoothLeScanner bleScanner = getScanner();
134
+ if (bleScanner == null) {
135
+ callback.onScanError("BLE scanner not available. Please enable Bluetooth and try again.");
136
+ return;
137
+ }
138
+
139
+ if (isScanning.get()) {
140
+ stopScan();
141
+ }
142
+
143
+ final List<BleDeviceInfo> foundDevices = new ArrayList<>();
144
+
145
+ activeScanCallback = new ScanCallback() {
146
+ @Override
147
+ public void onScanResult(int callbackType, ScanResult result) {
148
+ BluetoothDevice device = result.getDevice();
149
+ String name = device.getName();
150
+ String address = device.getAddress();
151
+ int rssi = result.getRssi();
152
+
153
+ // Show all devices - use address as name if no name available
154
+ if (name == null || name.isEmpty()) {
155
+ name = address;
156
+ }
157
+
158
+ // Check if already in list
159
+ boolean exists = false;
160
+ synchronized (foundDevices) {
161
+ for (BleDeviceInfo info : foundDevices) {
162
+ if (info.address.equals(address)) {
163
+ exists = true;
164
+ break;
165
+ }
166
+ }
167
+
168
+ if (!exists) {
169
+ BleDeviceInfo info = new BleDeviceInfo(name, address, rssi);
170
+ foundDevices.add(info);
171
+ }
172
+ }
173
+
174
+ if (!exists) {
175
+ final String finalName = name;
176
+ mainHandler.post(() -> callback.onDeviceFound(finalName, address, rssi));
177
+ }
178
+ }
179
+
180
+ @Override
181
+ public void onScanFailed(int errorCode) {
182
+ isScanning.set(false);
183
+ activeScanCallback = null;
184
+ String errorMsg = "Scan failed: ";
185
+ switch (errorCode) {
186
+ case ScanCallback.SCAN_FAILED_ALREADY_STARTED:
187
+ errorMsg += "Already started";
188
+ break;
189
+ case ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED:
190
+ errorMsg += "App registration failed";
191
+ break;
192
+ case ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED:
193
+ errorMsg += "Feature unsupported";
194
+ break;
195
+ case ScanCallback.SCAN_FAILED_INTERNAL_ERROR:
196
+ errorMsg += "Internal error";
197
+ break;
198
+ default:
199
+ errorMsg += "Error code " + errorCode;
200
+ }
201
+ final String finalErrorMsg = errorMsg;
202
+ mainHandler.post(() -> callback.onScanError(finalErrorMsg));
203
+ }
204
+ };
205
+
206
+ // Use low latency scan settings for faster discovery
207
+ ScanSettings scanSettings = new ScanSettings.Builder()
208
+ .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
209
+ .build();
210
+
211
+ isScanning.set(true);
212
+ Log.d(TAG, "Starting BLE scan with low latency mode...");
213
+ bleScanner.startScan(null, scanSettings, activeScanCallback);
214
+
215
+ // Stop scan after duration
216
+ mainHandler.postDelayed(() -> {
217
+ if (isScanning.get() && activeScanCallback != null) {
218
+ try {
219
+ bleScanner.stopScan(activeScanCallback);
220
+ } catch (Exception e) {
221
+ Log.w(TAG, "Error stopping scan", e);
222
+ }
223
+ isScanning.set(false);
224
+ activeScanCallback = null;
225
+
226
+ synchronized (foundDevices) {
227
+ callback.onScanComplete(new ArrayList<>(foundDevices));
228
+ }
229
+ }
230
+ }, durationMs);
231
+ }
232
+
233
+ public void stopScan() {
234
+ if (isScanning.get() && activeScanCallback != null) {
235
+ BluetoothLeScanner bleScanner = getScanner();
236
+ if (bleScanner != null) {
237
+ try {
238
+ bleScanner.stopScan(activeScanCallback);
239
+ } catch (Exception e) {
240
+ Log.w(TAG, "Error stopping scan", e);
241
+ }
242
+ }
243
+ isScanning.set(false);
244
+ activeScanCallback = null;
245
+ }
246
+ }
247
+
248
+ // ==================== CONNECTION ====================
249
+
250
+ public void connect(String address, int width, ConnectionCallback callback) {
251
+ this.connectionCallback = callback;
252
+ this.paperWidth = width;
253
+
254
+ if (bluetoothAdapter == null) {
255
+ callback.onError("Bluetooth not available");
256
+ return;
257
+ }
258
+
259
+ // Disconnect existing connection
260
+ if (gatt != null) {
261
+ disconnect();
262
+ }
263
+
264
+ BluetoothDevice device = bluetoothAdapter.getRemoteDevice(address);
265
+ if (device == null) {
266
+ callback.onError("Device not found: " + address);
267
+ return;
268
+ }
269
+
270
+ gatt = device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE);
271
+ }
272
+
273
+ public void disconnect() {
274
+ if (gatt != null) {
275
+ gatt.disconnect();
276
+ gatt.close();
277
+ gatt = null;
278
+ }
279
+ txCharacteristic = null;
280
+ connectedAddress = null;
281
+ isPaused.set(false);
282
+ writeQueue.clear();
283
+ }
284
+
285
+ public boolean isConnected() {
286
+ return gatt != null && txCharacteristic != null;
287
+ }
288
+
289
+ public String getConnectedAddress() {
290
+ return connectedAddress;
291
+ }
292
+
293
+ public int getPaperWidth() {
294
+ return paperWidth;
295
+ }
296
+
297
+ private final BluetoothGattCallback gattCallback = new BluetoothGattCallback() {
298
+ @Override
299
+ public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
300
+ if (newState == BluetoothProfile.STATE_CONNECTED) {
301
+ Log.d(TAG, "Connected to GATT server");
302
+ gatt.discoverServices();
303
+ } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
304
+ Log.d(TAG, "Disconnected from GATT server");
305
+ connectedAddress = null;
306
+ txCharacteristic = null;
307
+ if (connectionCallback != null) {
308
+ mainHandler.post(() -> connectionCallback.onDisconnected());
309
+ }
310
+ }
311
+ }
312
+
313
+ @Override
314
+ public void onServicesDiscovered(BluetoothGatt gatt, int status) {
315
+ if (status == BluetoothGatt.GATT_SUCCESS) {
316
+ // Find the printer service and characteristics
317
+ for (BluetoothGattService service : gatt.getServices()) {
318
+ BluetoothGattCharacteristic tx = service.getCharacteristic(PrinterProtocol.TX_CHAR_UUID);
319
+ BluetoothGattCharacteristic rx = service.getCharacteristic(PrinterProtocol.RX_CHAR_UUID);
320
+
321
+ if (tx != null && rx != null) {
322
+ txCharacteristic = tx;
323
+ connectedAddress = gatt.getDevice().getAddress();
324
+
325
+ // Enable notifications on RX for flow control
326
+ gatt.setCharacteristicNotification(rx, true);
327
+ BluetoothGattDescriptor descriptor = rx.getDescriptor(PrinterProtocol.CCCD_UUID);
328
+ if (descriptor != null) {
329
+ descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
330
+ gatt.writeDescriptor(descriptor);
331
+ }
332
+
333
+ if (connectionCallback != null) {
334
+ mainHandler.post(() -> connectionCallback.onConnected());
335
+ }
336
+ return;
337
+ }
338
+ }
339
+
340
+ // Characteristics not found
341
+ if (connectionCallback != null) {
342
+ mainHandler.post(() -> connectionCallback.onError("Printer characteristics not found"));
343
+ }
344
+ } else {
345
+ if (connectionCallback != null) {
346
+ mainHandler.post(() -> connectionCallback.onError("Service discovery failed"));
347
+ }
348
+ }
349
+ }
350
+
351
+ @Override
352
+ public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
353
+ byte[] data = characteristic.getValue();
354
+ if (data != null) {
355
+ if (PrinterProtocol.isFlowPause(data)) {
356
+ isPaused.set(true);
357
+ Log.d(TAG, "Flow control: PAUSE");
358
+ } else if (PrinterProtocol.isFlowResume(data)) {
359
+ isPaused.set(false);
360
+ Log.d(TAG, "Flow control: RESUME");
361
+ processWriteQueue();
362
+ }
363
+ }
364
+ }
365
+
366
+ @Override
367
+ public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
368
+ isWriting.set(false);
369
+ if (status == BluetoothGatt.GATT_SUCCESS) {
370
+ processWriteQueue();
371
+ } else {
372
+ Log.e(TAG, "Write failed with status: " + status);
373
+ }
374
+ }
375
+ };
376
+
377
+ // ==================== WRITING ====================
378
+
379
+ private void queueWrite(byte[] data) {
380
+ // Split into MTU-sized chunks
381
+ int offset = 0;
382
+ while (offset < data.length) {
383
+ int chunkSize = Math.min(MTU_SIZE, data.length - offset);
384
+ byte[] chunk = new byte[chunkSize];
385
+ System.arraycopy(data, offset, chunk, 0, chunkSize);
386
+ writeQueue.add(chunk);
387
+ offset += chunkSize;
388
+ }
389
+ processWriteQueue();
390
+ }
391
+
392
+ private void processWriteQueue() {
393
+ if (isPaused.get() || isWriting.get() || writeQueue.isEmpty()) {
394
+ return;
395
+ }
396
+
397
+ if (gatt == null || txCharacteristic == null) {
398
+ writeQueue.clear();
399
+ return;
400
+ }
401
+
402
+ byte[] data = writeQueue.poll();
403
+ if (data != null) {
404
+ isWriting.set(true);
405
+ txCharacteristic.setValue(data);
406
+ txCharacteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);
407
+
408
+ boolean success = gatt.writeCharacteristic(txCharacteristic);
409
+ if (!success) {
410
+ isWriting.set(false);
411
+ Log.e(TAG, "Failed to initiate write");
412
+ }
413
+
414
+ // Small delay between writes
415
+ mainHandler.postDelayed(this::processWriteQueue, WRITE_DELAY_MS);
416
+ }
417
+ }
418
+
419
+ private void flushQueue() {
420
+ // Wait for queue to empty with timeout
421
+ int maxWaitMs = 60000; // 60 second timeout for long images
422
+ int waited = 0;
423
+ int sleepInterval = 50;
424
+
425
+ while ((!writeQueue.isEmpty() || isWriting.get()) && waited < maxWaitMs) {
426
+ try {
427
+ Thread.sleep(sleepInterval);
428
+ waited += sleepInterval;
429
+
430
+ // If paused, wait but don't count against timeout
431
+ if (isPaused.get()) {
432
+ waited = Math.max(0, waited - sleepInterval);
433
+ }
434
+ } catch (InterruptedException e) {
435
+ Thread.currentThread().interrupt();
436
+ break;
437
+ }
438
+ }
439
+
440
+ if (waited >= maxWaitMs) {
441
+ Log.w(TAG, "Flush timeout - queue may not be empty");
442
+ }
443
+ }
444
+
445
+ // ==================== PRINTING ====================
446
+
447
+ public void printImage(String base64Image, float energy, int quality,
448
+ int feedAfter, int threshold, boolean dither, PrintCallback callback) {
449
+ this.printCallback = callback;
450
+
451
+ if (!isConnected()) {
452
+ callback.onError("Not connected to printer");
453
+ return;
454
+ }
455
+
456
+ if (isPrinting.get()) {
457
+ callback.onError("Already printing");
458
+ return;
459
+ }
460
+
461
+ isPrinting.set(true);
462
+
463
+ new Thread(() -> {
464
+ try {
465
+ reportProgress(0, "processing", "Decoding image");
466
+
467
+ // Decode base64 image
468
+ byte[] imageBytes = Base64.decode(base64Image, Base64.DEFAULT);
469
+ Bitmap bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
470
+
471
+ if (bitmap == null) {
472
+ throw new Exception("Failed to decode image");
473
+ }
474
+
475
+ Log.d(TAG, "Original image size: " + bitmap.getWidth() + "x" + bitmap.getHeight());
476
+
477
+ reportProgress(10, "processing", "Resizing image");
478
+
479
+ // Resize to paper width
480
+ bitmap = ImageProcessor.resizeToWidth(bitmap, paperWidth);
481
+
482
+ Log.d(TAG, "Resized image size: " + bitmap.getWidth() + "x" + bitmap.getHeight());
483
+
484
+ reportProgress(20, "processing", "Converting to monochrome");
485
+
486
+ // Convert to 1-bit monochrome (use dithering for better quality)
487
+ byte[] monoData;
488
+ if (dither) {
489
+ monoData = ImageProcessor.toMonochromeDithered(bitmap, threshold);
490
+ } else {
491
+ monoData = ImageProcessor.toMonochrome(bitmap, threshold);
492
+ }
493
+ int height = bitmap.getHeight();
494
+
495
+ Log.d(TAG, "Printing " + height + " lines");
496
+
497
+ bitmap.recycle();
498
+
499
+ // Print the bitmap
500
+ printBitmapData(monoData, paperWidth, height, energy, quality, feedAfter);
501
+
502
+ } catch (Exception e) {
503
+ Log.e(TAG, "Print error", e);
504
+ isPrinting.set(false);
505
+ mainHandler.post(() -> callback.onError(e.getMessage()));
506
+ }
507
+ }).start();
508
+ }
509
+
510
+ public void printText(String text, int fontSize, String align, boolean bold,
511
+ float lineSpacing, float energy, int quality,
512
+ int feedAfter, PrintCallback callback) {
513
+ this.printCallback = callback;
514
+
515
+ if (!isConnected()) {
516
+ callback.onError("Not connected to printer");
517
+ return;
518
+ }
519
+
520
+ if (isPrinting.get()) {
521
+ callback.onError("Already printing");
522
+ return;
523
+ }
524
+
525
+ isPrinting.set(true);
526
+
527
+ new Thread(() -> {
528
+ try {
529
+ reportProgress(0, "processing", "Rendering text");
530
+
531
+ // Render text to bitmap
532
+ Bitmap bitmap = ImageProcessor.renderText(text, paperWidth, fontSize,
533
+ align, bold, lineSpacing);
534
+
535
+ reportProgress(20, "processing", "Converting to monochrome");
536
+
537
+ // Convert to 1-bit (use lower threshold for text - sharper)
538
+ byte[] monoData = ImageProcessor.toMonochrome(bitmap, 127);
539
+ int height = bitmap.getHeight();
540
+
541
+ bitmap.recycle();
542
+
543
+ // Print the bitmap
544
+ printBitmapData(monoData, paperWidth, height, energy, quality, feedAfter);
545
+
546
+ } catch (Exception e) {
547
+ Log.e(TAG, "Print error", e);
548
+ isPrinting.set(false);
549
+ mainHandler.post(() -> callback.onError(e.getMessage()));
550
+ }
551
+ }).start();
552
+ }
553
+
554
+ private void printBitmapData(byte[] data, int width, int height,
555
+ float energy, int quality, int feedAfter) {
556
+ try {
557
+ int bytesPerLine = width / 8;
558
+ int totalLines = height;
559
+
560
+ reportProgress(30, "printing", "Initializing printer");
561
+
562
+ // Calculate energy and speed values
563
+ // Reference: energy = int(args.energy * 0xffff)
564
+ int energyValue = (int) (energy * 0xffff);
565
+ // Reference: speed = 4 * (quality + 5), quality 1-4 -> speed 24-36
566
+ int speedValue = 4 * (quality + 5);
567
+
568
+ // === PREPARE PHASE (matches reference _prepare method) ===
569
+ // 1. Get device state
570
+ queueWrite(PrinterProtocol.cmdGetDeviceState());
571
+
572
+ // 2. Start printing (use standard command, not new)
573
+ queueWrite(PrinterProtocol.cmdStartPrinting());
574
+
575
+ // 3. Set DPI
576
+ queueWrite(PrinterProtocol.cmdSetDpi200());
577
+
578
+ // 4. Set speed (if specified)
579
+ if (speedValue > 0) {
580
+ queueWrite(PrinterProtocol.cmdSetSpeed(speedValue));
581
+ }
582
+
583
+ // 5. Set energy (if specified)
584
+ if (energyValue > 0) {
585
+ queueWrite(PrinterProtocol.cmdSetEnergy(energyValue));
586
+ }
587
+
588
+ // 6. Apply energy
589
+ queueWrite(PrinterProtocol.cmdApplyEnergy());
590
+
591
+ // 7. Update device
592
+ queueWrite(PrinterProtocol.cmdUpdateDevice());
593
+
594
+ // 8. Flush before starting lattice (important!)
595
+ flushQueue();
596
+
597
+ // 9. Start lattice
598
+ queueWrite(PrinterProtocol.cmdStartLattice());
599
+
600
+ // === PRINT BITMAP DATA ===
601
+ for (int line = 0; line < totalLines; line++) {
602
+ int offset = line * bytesPerLine;
603
+ byte[] lineData = new byte[bytesPerLine];
604
+ System.arraycopy(data, offset, lineData, 0, bytesPerLine);
605
+
606
+ queueWrite(PrinterProtocol.cmdDrawBitmap(lineData));
607
+
608
+ // Report progress every 50 lines
609
+ if (line % 50 == 0) {
610
+ int progress = 30 + (int) ((line / (float) totalLines) * 60);
611
+ reportProgress(progress, "printing", "Printing line " + line + "/" + totalLines);
612
+ }
613
+ }
614
+
615
+ reportProgress(90, "feeding", "Finishing");
616
+
617
+ // === FINISH PHASE (matches reference _finish method) ===
618
+ // 1. End lattice
619
+ queueWrite(PrinterProtocol.cmdEndLattice());
620
+
621
+ // 2. Set speed to 8 for feeding
622
+ queueWrite(PrinterProtocol.cmdSetSpeed(8));
623
+
624
+ // 3. Feed paper
625
+ queueWrite(PrinterProtocol.cmdFeedPaper(feedAfter));
626
+
627
+ // 4. Get device state
628
+ queueWrite(PrinterProtocol.cmdGetDeviceState());
629
+
630
+ // 5. Final flush
631
+ flushQueue();
632
+
633
+ isPrinting.set(false);
634
+ reportProgress(100, "done", "Complete");
635
+
636
+ if (printCallback != null) {
637
+ mainHandler.post(() -> printCallback.onComplete());
638
+ }
639
+
640
+ } catch (Exception e) {
641
+ isPrinting.set(false);
642
+ throw new RuntimeException(e);
643
+ }
644
+ }
645
+
646
+ public void feedPaper(int pixels, PrintCallback callback) {
647
+ if (!isConnected()) {
648
+ callback.onError("Not connected to printer");
649
+ return;
650
+ }
651
+
652
+ queueWrite(PrinterProtocol.cmdFeedPaper(pixels));
653
+ callback.onComplete();
654
+ }
655
+
656
+ private void reportProgress(int percent, String status, String message) {
657
+ if (printCallback != null) {
658
+ mainHandler.post(() -> printCallback.onProgress(percent, status, message));
659
+ }
660
+ }
661
+ }