@capgo/capacitor-printer 8.1.2 → 8.1.3

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.
@@ -14,6 +14,7 @@ import android.print.PageRange;
14
14
  import android.print.PrintAttributes;
15
15
  import android.print.PrintDocumentAdapter;
16
16
  import android.print.PrintDocumentInfo;
17
+ import android.print.PrintJob;
17
18
  import android.print.PrintManager;
18
19
  import android.util.Base64;
19
20
  import android.webkit.WebView;
@@ -26,9 +27,26 @@ import java.io.FileOutputStream;
26
27
  import java.io.IOException;
27
28
  import java.io.InputStream;
28
29
  import java.io.OutputStream;
30
+ import java.util.concurrent.atomic.AtomicBoolean;
31
+ import java.util.concurrent.atomic.AtomicReference;
29
32
 
30
33
  public class Printer {
31
34
 
35
+ /**
36
+ * Reports the end of a print session back to the caller.
37
+ */
38
+ public interface OnPrintFinishCallback {
39
+ /**
40
+ * Invoked once the print session has ended, whether the user printed or cancelled.
41
+ */
42
+ void onFinish();
43
+
44
+ /**
45
+ * Invoked when the job could not be started at all.
46
+ */
47
+ void onError(String message);
48
+ }
49
+
32
50
  private final Context context;
33
51
  private final Activity activity;
34
52
 
@@ -168,6 +186,19 @@ public class Printer {
168
186
  }
169
187
 
170
188
  public void printWebView(WebView webView, String name) throws Exception {
189
+ printWebView(webView, name, null);
190
+ }
191
+
192
+ /**
193
+ * Prints the current content of the web view.
194
+ *
195
+ * <p>The adapter returned by {@link WebView#createPrintDocumentAdapter(String)} renders lazily
196
+ * from the live web view, so callers must keep that content in place until {@code callback}
197
+ * reports the session has finished.
198
+ *
199
+ * @param callback notified once the print session ends, or null for fire-and-forget printing.
200
+ */
201
+ public void printWebView(WebView webView, String name, OnPrintFinishCallback callback) throws Exception {
171
202
  if (webView == null) {
172
203
  throw new Exception("WebView not available");
173
204
  }
@@ -175,7 +206,15 @@ public class Printer {
175
206
  new Runnable() {
176
207
  @Override
177
208
  public void run() {
178
- createWebPrintJob(webView, name);
209
+ try {
210
+ createWebPrintJob(webView, name, callback);
211
+ } catch (Exception e) {
212
+ // The job is created asynchronously on the UI thread, so failures here
213
+ // cannot surface through the throws clause above.
214
+ if (callback != null) {
215
+ callback.onError(e.getMessage());
216
+ }
217
+ }
179
218
  }
180
219
  }
181
220
  );
@@ -229,13 +268,60 @@ public class Printer {
229
268
  }
230
269
 
231
270
  private void createWebPrintJob(WebView webView, String name) {
271
+ try {
272
+ createWebPrintJob(webView, name, null);
273
+ } catch (Exception e) {
274
+ // Preserves the previous silent behaviour for callers without a callback.
275
+ }
276
+ }
277
+
278
+ private void createWebPrintJob(WebView webView, String name, OnPrintFinishCallback callback) throws Exception {
232
279
  PrintManager printManager = (PrintManager) context.getSystemService(Context.PRINT_SERVICE);
233
280
  if (printManager == null) {
234
- return;
281
+ throw new Exception("Print service not available");
235
282
  }
236
283
 
237
284
  PrintDocumentAdapter printAdapter = webView.createPrintDocumentAdapter(name);
238
- printManager.print(name, printAdapter, new PrintAttributes.Builder().build());
285
+
286
+ if (callback == null) {
287
+ printManager.print(name, printAdapter, new PrintAttributes.Builder().build());
288
+ return;
289
+ }
290
+
291
+ // Populated right after print() returns. We are on the UI thread here and the adapter
292
+ // callbacks are dispatched to that same thread, so none of them can run before the
293
+ // reference is set.
294
+ AtomicReference<PrintJob> printJob = new AtomicReference<>();
295
+
296
+ // The framework calls onFinish() on the wrapped adapter once the print session ends,
297
+ // which is the earliest point at which the web view content is no longer needed.
298
+ PrintDocumentAdapter proxy = new PrintFinishAdapter(printAdapter, () -> {
299
+ PrintJob job = printJob.get();
300
+
301
+ // Layout and write failures are reported to the framework's own result callbacks,
302
+ // which cannot be intercepted — PrintDocumentAdapter.LayoutResultCallback and
303
+ // WriteResultCallback are not subclassable outside android.print. The resulting job
304
+ // state is the only signal available to us, and it is best effort: a job that fails
305
+ // after the session ends (offline printer) settles too late to be seen here.
306
+ if (job != null && job.isFailed()) {
307
+ callback.onError("Print job failed");
308
+ return;
309
+ }
310
+
311
+ callback.onFinish();
312
+ });
313
+
314
+ PrintJob job = printManager.print(name, proxy, new PrintAttributes.Builder().build());
315
+
316
+ // print() is documented to return null on failure (despite its @NonNull annotation) and
317
+ // does so when the print feature is missing or the print dialog cannot be started. The
318
+ // adapter is never invoked in that case, so onFinish() would never arrive.
319
+ if (job == null) {
320
+ callback.onError("Failed to start print job");
321
+ return;
322
+ }
323
+
324
+ printJob.set(job);
239
325
  }
240
326
 
241
327
  private File saveTempFile(byte[] data, String mimeType) throws IOException {
@@ -282,6 +368,61 @@ public class Printer {
282
368
  }
283
369
  }
284
370
 
371
+ /**
372
+ * Delegating adapter that additionally notifies when the print session has finished.
373
+ */
374
+ private static class PrintFinishAdapter extends PrintDocumentAdapter {
375
+
376
+ private final PrintDocumentAdapter delegate;
377
+ private final Runnable onFinished;
378
+ private final AtomicBoolean notified = new AtomicBoolean(false);
379
+
380
+ PrintFinishAdapter(@NonNull PrintDocumentAdapter delegate, @NonNull Runnable onFinished) {
381
+ this.delegate = delegate;
382
+ this.onFinished = onFinished;
383
+ }
384
+
385
+ @Override
386
+ public void onStart() {
387
+ delegate.onStart();
388
+ }
389
+
390
+ @Override
391
+ public void onLayout(
392
+ PrintAttributes oldAttributes,
393
+ PrintAttributes newAttributes,
394
+ CancellationSignal cancellationSignal,
395
+ LayoutResultCallback callback,
396
+ Bundle extras
397
+ ) {
398
+ delegate.onLayout(oldAttributes, newAttributes, cancellationSignal, callback, extras);
399
+ }
400
+
401
+ @Override
402
+ public void onWrite(
403
+ PageRange[] pages,
404
+ ParcelFileDescriptor destination,
405
+ CancellationSignal cancellationSignal,
406
+ WriteResultCallback callback
407
+ ) {
408
+ delegate.onWrite(pages, destination, cancellationSignal, callback);
409
+ }
410
+
411
+ @Override
412
+ public void onFinish() {
413
+ try {
414
+ // Let the WebView adapter release its own resources first.
415
+ delegate.onFinish();
416
+ } finally {
417
+ // Runs even if the delegate throws during teardown, so the caller is always
418
+ // notified exactly once.
419
+ if (notified.compareAndSet(false, true)) {
420
+ onFinished.run();
421
+ }
422
+ }
423
+ }
424
+ }
425
+
285
426
  // Custom PrintDocumentAdapter for PDF files
286
427
  private class PdfDocumentAdapter extends PrintDocumentAdapter {
287
428
 
@@ -9,7 +9,7 @@ import com.getcapacitor.annotation.CapacitorPlugin;
9
9
  @CapacitorPlugin(name = "Printer")
10
10
  public class PrinterPlugin extends Plugin {
11
11
 
12
- private final String pluginVersion = "8.1.2";
12
+ private final String pluginVersion = "8.1.3";
13
13
 
14
14
  private Printer implementation;
15
15
 
@@ -120,8 +120,25 @@ public class PrinterPlugin extends Plugin {
120
120
  String name = call.getString("name", "Document");
121
121
 
122
122
  try {
123
- implementation.printWebView(getBridge().getWebView(), name);
124
- call.resolve();
123
+ // Resolve once the print session ends rather than once the job is handed to the
124
+ // system: the WebView still backs the print adapter until then, so resolving early
125
+ // lets callers navigate away while pages are still being rendered.
126
+ implementation.printWebView(
127
+ getBridge().getWebView(),
128
+ name,
129
+ new Printer.OnPrintFinishCallback() {
130
+ @Override
131
+ public void onFinish() {
132
+ // A cancelled job is a normal outcome, not a failure.
133
+ call.resolve();
134
+ }
135
+
136
+ @Override
137
+ public void onError(String message) {
138
+ call.reject("Failed to print web view: " + message);
139
+ }
140
+ }
141
+ );
125
142
  } catch (Exception e) {
126
143
  call.reject("Failed to print web view: " + e.getMessage(), e);
127
144
  }
@@ -10,6 +10,7 @@ import WebKit
10
10
  case unsupportedMimeType
11
11
  case printingNotAvailable
12
12
  case invalidData
13
+ case presentationFailed
13
14
  }
14
15
 
15
16
  /// Print base64 encoded data
@@ -175,10 +176,15 @@ import WebKit
175
176
  }
176
177
 
177
178
  /// Print web view content
179
+ ///
180
+ /// `completion` is invoked once the print interaction has been dismissed, whether the user
181
+ /// printed or cancelled. `viewPrintFormatter()` renders lazily from the live web view, so
182
+ /// callers must keep that content in place until `completion` fires.
178
183
  public func printWebView(
179
184
  webView: WKWebView,
180
185
  name: String,
181
- presentingViewController: UIViewController?
186
+ presentingViewController: UIViewController?,
187
+ completion: ((Bool, Error?) -> Void)? = nil
182
188
  ) throws {
183
189
  let printInfo = UIPrintInfo(dictionary: nil)
184
190
  printInfo.jobName = name
@@ -190,7 +196,8 @@ import WebKit
190
196
  printInfo: printInfo,
191
197
  printFormatter: formatter,
192
198
  printItem: nil,
193
- presentingViewController: presentingViewController
199
+ presentingViewController: presentingViewController,
200
+ completion: completion
194
201
  )
195
202
  }
196
203
 
@@ -200,7 +207,8 @@ import WebKit
200
207
  printInfo: UIPrintInfo,
201
208
  printFormatter: UIPrintFormatter?,
202
209
  printItem: Any?,
203
- presentingViewController: UIViewController?
210
+ presentingViewController: UIViewController?,
211
+ completion: ((Bool, Error?) -> Void)? = nil
204
212
  ) throws {
205
213
  guard UIPrintInteractionController.isPrintingAvailable else {
206
214
  throw PrinterError.printingNotAvailable
@@ -219,21 +227,41 @@ import WebKit
219
227
  printController.printingItem = item
220
228
  }
221
229
 
230
+ // Only build a handler when a caller asked for one, so callers that opted out keep the
231
+ // existing fire-and-forget behaviour.
232
+ var handler: UIPrintInteractionController.CompletionHandler?
233
+
234
+ if let completion = completion {
235
+ handler = { _, completed, error in
236
+ completion(completed, error)
237
+ }
238
+ }
239
+
222
240
  // Present print controller
241
+ let presented: Bool
242
+
223
243
  if UIDevice.current.userInterfaceIdiom == .pad {
224
244
  // For iPad, present as popover
225
- printController.present(
245
+ presented = printController.present(
226
246
  from: viewController.view.bounds,
227
247
  in: viewController.view,
228
248
  animated: true,
229
- completionHandler: nil
249
+ completionHandler: handler
230
250
  )
231
251
  } else {
232
252
  // For iPhone, present modally
233
- printController.present(
253
+ presented = printController.present(
234
254
  animated: true,
235
- completionHandler: nil
255
+ completionHandler: handler
236
256
  )
237
257
  }
258
+
259
+ // A false return means nothing was shown and the completion handler will never be
260
+ // scheduled — for example when another print interaction is already presented, since
261
+ // UIPrintInteractionController.shared is a singleton. Callers waiting on that handler
262
+ // would otherwise never hear back.
263
+ if !presented {
264
+ throw PrinterError.presentationFailed
265
+ }
238
266
  }
239
267
  }
@@ -3,7 +3,7 @@ import Capacitor
3
3
 
4
4
  @objc(PrinterPlugin)
5
5
  public class PrinterPlugin: CAPPlugin, CAPBridgedPlugin {
6
- private let pluginVersion: String = "8.1.2"
6
+ private let pluginVersion: String = "8.1.3"
7
7
  public let identifier = "PrinterPlugin"
8
8
  public let jsName = "Printer"
9
9
  public let pluginMethods: [CAPPluginMethod] = [
@@ -159,12 +159,21 @@ public class PrinterPlugin: CAPPlugin, CAPBridgedPlugin {
159
159
  }
160
160
 
161
161
  do {
162
+ // Resolve from the print interaction's completion handler rather than straight
163
+ // after presenting: the web view still backs the print formatter until the sheet
164
+ // is dismissed, so resolving early lets callers navigate away mid-render.
162
165
  try self.implementation.printWebView(
163
166
  webView: webView,
164
167
  name: name,
165
168
  presentingViewController: self.bridge?.viewController
166
- )
167
- call.resolve()
169
+ ) { _, error in
170
+ if let error = error {
171
+ call.reject("Failed to print web view: \(error.localizedDescription)")
172
+ } else {
173
+ // A user-cancelled sheet is a normal outcome, not a failure.
174
+ call.resolve()
175
+ }
176
+ }
168
177
  } catch {
169
178
  call.reject("Failed to print web view: \(error.localizedDescription)")
170
179
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-printer",
3
- "version": "8.1.2",
3
+ "version": "8.1.3",
4
4
  "description": "Capacitor plugin for printing documents, HTML, PDFs, images and web views",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",