From 884e5c4862c71735bcffaf77807484ccc4dcbd2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Katzer?= Date: Tue, 29 Jan 2019 12:22:20 +0100 Subject: [PATCH] Improvements and fixes for Android --- plugin.xml | 8 +- ...PrintPdfAdapter.java => PrintAdapter.java} | 61 ++++--- .../{AssetUtil.java => PrintContent.java} | 156 ++++++++++++++---- src/android/PrintManager.java | 43 +++-- src/android/PrintOptions.java | 21 +-- src/ios/APPPrinterInfo.m | 2 +- src/windows/PrinterProxy.js | 2 +- www/printer.js | 12 +- 8 files changed, 211 insertions(+), 94 deletions(-) rename src/android/{PrintPdfAdapter.java => PrintAdapter.java} (64%) rename src/android/{AssetUtil.java => PrintContent.java} (67%) diff --git a/plugin.xml b/plugin.xml index 32bf1f0..9ae64d9 100644 --- a/plugin.xml +++ b/plugin.xml @@ -101,7 +101,10 @@ - + + - - diff --git a/src/android/PrintPdfAdapter.java b/src/android/PrintAdapter.java similarity index 64% rename from src/android/PrintPdfAdapter.java rename to src/android/PrintAdapter.java index df099b9..35bbfd7 100644 --- a/src/android/PrintPdfAdapter.java +++ b/src/android/PrintAdapter.java @@ -19,7 +19,6 @@ package de.appplant.cordova.plugin.printer; -import android.content.Context; import android.os.Bundle; import android.os.CancellationSignal; import android.os.ParcelFileDescriptor; @@ -28,6 +27,8 @@ import android.print.PrintAttributes; import android.print.PrintDocumentAdapter; import android.print.PrintDocumentInfo; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.print.PrintHelper; import java.io.FileOutputStream; import java.io.IOException; @@ -39,23 +40,30 @@ import static android.print.PrintDocumentInfo.CONTENT_TYPE_DOCUMENT; /** * Document adapter to render and print PDF files. */ -class PrintPdfAdapter extends PrintDocumentAdapter { +class PrintAdapter extends PrintDocumentAdapter { - // The application context - private @NonNull Context context; + // The name of the print job + private final @NonNull String jobName; - // The path to the PDF file - private @NonNull String path; + // The input stream to render + private final @NonNull InputStream input; + + // The callback to inform once the job is done + private final @Nullable PrintHelper.OnPrintFinishCallback callback; /** * Constructor * - * @param context The context where to look for. + * @param jobName The name of the print job. + * @param input The input stream to render. + * @param callback The callback to inform once the job is done. */ - PrintPdfAdapter (@NonNull String path, @NonNull Context context) + PrintAdapter (@NonNull String jobName, @NonNull InputStream input, + @Nullable PrintHelper.OnPrintFinishCallback callback) { - this.path = path; - this.context = context; + this.jobName = jobName; + this.input = input; + this.callback = callback; } @Override @@ -70,11 +78,13 @@ class PrintPdfAdapter extends PrintDocumentAdapter { if (cancellationSignal.isCanceled()) return; - pdi = new PrintDocumentInfo.Builder("test") + pdi = new PrintDocumentInfo.Builder(jobName) .setContentType(CONTENT_TYPE_DOCUMENT) .build(); - callback.onLayoutFinished(pdi, true); + boolean changed = !newAttributes.equals(oldAttributes); + + callback.onLayoutFinished(pdi, changed); } @Override @@ -86,18 +96,10 @@ class PrintPdfAdapter extends PrintDocumentAdapter { if (cancellationSignal.isCanceled()) return; - AssetUtil io = new AssetUtil(context); - InputStream in = io.open(path); - - if (in == null) { - callback.onWriteFailed("File not found: " + path); - return; - } - - OutputStream out = new FileOutputStream(dest.getFileDescriptor()); + OutputStream output = new FileOutputStream(dest.getFileDescriptor()); try { - AssetUtil.copy(in, out); + PrintContent.copy(input, output); } catch (IOException e) { callback.onWriteFailed(e.getMessage()); return; @@ -105,4 +107,19 @@ class PrintPdfAdapter extends PrintDocumentAdapter { callback.onWriteFinished(new PageRange[]{ PageRange.ALL_PAGES }); } + + /** + * Close input stream once the printing is done. + */ + @Override + public void onFinish () + { + super.onFinish(); + + PrintContent.close(input); + + if (callback != null) { + callback.onFinish(); + } + } } diff --git a/src/android/AssetUtil.java b/src/android/PrintContent.java similarity index 67% rename from src/android/AssetUtil.java rename to src/android/PrintContent.java index d9e7bc4..8e5e140 100644 --- a/src/android/AssetUtil.java +++ b/src/android/PrintContent.java @@ -29,16 +29,18 @@ import android.support.annotation.Nullable; import android.util.Base64; import java.io.ByteArrayInputStream; +import java.io.Closeable; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.net.URLConnection; -class AssetUtil { +class PrintContent { // List of supported content types - enum ContentType { PLAIN, HTML, IMAGE, PDF, SELF } + enum ContentType { PLAIN, HTML, IMAGE, PDF, UNSUPPORTED } // Application context private final @NonNull Context context; @@ -48,8 +50,8 @@ class AssetUtil { * * @param ctx The application context. */ - AssetUtil (@NonNull Context ctx) { - this.context = ctx; + private PrintContent (@NonNull Context ctx) { + context = ctx; } /** @@ -59,21 +61,60 @@ class AssetUtil { * * @return The content type even the file does not exist. */ - static @NonNull ContentType getContentType (@Nullable String path) + @NonNull + static ContentType getContentType (@Nullable String path, + @NonNull Context context) + { + return new PrintContent(context).getContentType(path); + } + + /** + * Returns the content type for the file referenced by its uri. + * + * @param path The path to check. + * + * @return The content type even the file does not exist. + */ + @NonNull + private ContentType getContentType (@Nullable String path) { ContentType type = ContentType.PLAIN; - if (path == null || path.isEmpty()) - { - type = ContentType.SELF; - } - else if (path.charAt(0) == '<') + if (path == null || path.isEmpty() || path.charAt(0) == '<') { type = ContentType.HTML; } - else if (path.matches("^[a-z]+://.+")) + else if (path.matches("^[a-z0-9]+://.+")) { - return path.endsWith(".pdf") ? ContentType.PDF : ContentType.IMAGE; + String mime; + + if (path.startsWith("base64:")) { + try { + mime = URLConnection.guessContentTypeFromStream(openBase64(path)); + } catch (IOException e) { + return ContentType.UNSUPPORTED; + } + } else { + mime = URLConnection.guessContentTypeFromName(path); + } + + switch (mime) + { + case "image/bmp": + case "image/png": + case "image/jpeg": + case "image/jpeg2000": + case "image/jp2": + case "image/gif": + case "image/x-icon": + case "image/vnd.microsoft.icon": + case "image/heif": + return ContentType.IMAGE; + case "application/pdf": + return ContentType.PDF; + default: + return ContentType.UNSUPPORTED; + } } return type; @@ -83,10 +124,25 @@ class AssetUtil { * Opens a file://, res:// or base64:// Uri as a stream. * * @param path The file path to decode. + * @param context The application context. * * @return An open IO stream or null if the file does not exist. */ - @Nullable InputStream open (@NonNull String path) + @Nullable + static InputStream open (@NonNull String path, @NonNull Context context) + { + return new PrintContent(context).open(path); + } + + /** + * Opens a file://, res:// or base64:// Uri as a stream. + * + * @param path The file path to decode. + * + * @return An open IO stream or null if the file does not exist. + */ + @Nullable + private InputStream open (@NonNull String path) { InputStream stream = null; @@ -110,6 +166,20 @@ class AssetUtil { return stream; } + /** + * Decodes a file://, res:// or base64:// Uri to bitmap. + * + * @param path The file path to decode. + * @param context The application context. + * + * @return A bitmap or null if the path is not valid + */ + @Nullable + static Bitmap decode (@NonNull String path, @NonNull Context context) + { + return new PrintContent(context).decode(path); + } + /** * Decodes a file://, res:// or base64:// Uri to bitmap. * @@ -117,7 +187,8 @@ class AssetUtil { * * @return A bitmap or null if the path is not valid */ - @Nullable Bitmap decode (@NonNull String path) + @Nullable + private Bitmap decode (@NonNull String path) { Bitmap bitmap; @@ -150,7 +221,8 @@ class AssetUtil { * @param input The readable input stream. * @param output The writable output stream. * - * @throws IOException + * @throws IOException If the input stream is not readable, + * or the output stream is not writable. */ static void copy (@NonNull InputStream input, @NonNull OutputStream output) throws IOException @@ -162,8 +234,22 @@ class AssetUtil { output.write(buf, 0, bytesRead); } - output.close(); - input.close(); + input.reset(); + close(output); + } + + /** + * Closes the stream. + * + * @param stream The stream to close. + */ + static void close (@NonNull Closeable stream) + { + try { + stream.close(); + } catch (IOException e) { + // ignore + } } /** @@ -173,7 +259,8 @@ class AssetUtil { * * @return An open IO stream or null if the file does not exist. */ - private @Nullable InputStream openFile (@NonNull String path) + @Nullable + private InputStream openFile (@NonNull String path) { String absPath = path.substring(7); @@ -191,7 +278,8 @@ class AssetUtil { * * @return A bitmap or null if the path is not valid */ - private @Nullable Bitmap decodeFile (@NonNull String path) + @Nullable + private Bitmap decodeFile (@NonNull String path) { String absPath = path.substring(7); @@ -205,7 +293,8 @@ class AssetUtil { * * @return An open IO stream or null if the file does not exist. */ - private @Nullable InputStream openAsset (@NonNull String path) + @Nullable + private InputStream openAsset (@NonNull String path) { String resPath = path.replaceFirst("file:/", "www"); @@ -223,7 +312,8 @@ class AssetUtil { * * @return A bitmap or null if the path is not valid */ - private @Nullable Bitmap decodeAsset (@NonNull String path) + @Nullable + private Bitmap decodeAsset (@NonNull String path) { InputStream stream = openAsset(path); Bitmap bitmap; @@ -233,11 +323,7 @@ class AssetUtil { bitmap = BitmapFactory.decodeStream(stream); - try { - stream.close(); - } catch (IOException e) { - // ignore - } + close(stream); return bitmap; } @@ -249,7 +335,8 @@ class AssetUtil { * * @return An open IO stream or null if the file does not exist. */ - private @NonNull InputStream openResource (@NonNull String path) + @NonNull + private InputStream openResource (@NonNull String path) { String resPath = path.substring(6); int resId = getResId(resPath); @@ -264,7 +351,8 @@ class AssetUtil { * * @return A bitmap or null if the path is not valid */ - private @Nullable Bitmap decodeResource (@NonNull String path) + @Nullable + private Bitmap decodeResource (@NonNull String path) { String data = path.substring(9); byte[] bytes = Base64.decode(data, 0); @@ -279,7 +367,8 @@ class AssetUtil { * * @return An open IO stream or null if the file does not exist. */ - private @NonNull InputStream openBase64 (@NonNull String path) + @NonNull + private InputStream openBase64 (@NonNull String path) { String data = path.substring(9); byte[] bytes = Base64.decode(data, 0); @@ -294,7 +383,8 @@ class AssetUtil { * * @return A bitmap or null if the path is not valid */ - private @Nullable Bitmap decodeBase64 (@NonNull String path) + @Nullable + private Bitmap decodeBase64 (@NonNull String path) { String data = path.substring(9); byte[] bytes = Base64.decode(data, 0); @@ -333,10 +423,16 @@ class AssetUtil { return resId; } + /** + * Returns the asset manager for the app. + */ private AssetManager getAssets() { return context.getAssets(); } + /** + * Returns the resource bundle for the app. + */ private Resources getResources() { return context.getResources(); } diff --git a/src/android/PrintManager.java b/src/android/PrintManager.java index 3b2c645..9f8f2e2 100644 --- a/src/android/PrintManager.java +++ b/src/android/PrintManager.java @@ -31,12 +31,15 @@ import android.support.v4.print.PrintHelper; import org.json.JSONArray; import org.json.JSONObject; +import java.io.InputStream; + import static android.content.Context.PRINT_SERVICE; +import static de.appplant.cordova.plugin.printer.PrintContent.ContentType.UNSUPPORTED; class PrintManager { // The application context - private @NonNull Context context; + private final @NonNull Context context; /** * Constructor @@ -61,15 +64,16 @@ class PrintManager { if (item != null) { - supported = new AssetUtil(context).open(item) != null; + supported = PrintContent.getContentType(item, context) != UNSUPPORTED; } return supported; } /** - * @return List of all printable document types (utis). + * List of all printable document types (utis). */ + @NonNull static JSONArray getPrintableUTIs() { JSONArray utis = new JSONArray(); @@ -82,8 +86,6 @@ class PrintManager { utis.put("public.heif"); utis.put("com.compuserve.gif"); utis.put("com.microsoft.ico"); - utis.put("com.microsoft.bmp"); - utis.put("com.microsoft.bmp"); return utis; } @@ -96,19 +98,20 @@ class PrintManager { * @param settings Additional settings how to render the content. * @param callback The function to invoke once the job is done. */ - void print (@Nullable String content, JSONObject settings, + void print (@Nullable String content, @NonNull JSONObject settings, @Nullable PrintHelper.OnPrintFinishCallback callback) { - switch (AssetUtil.getContentType(content)) + switch (PrintContent.getContentType(content, context)) { case IMAGE: + //noinspection ConstantConditions printImage(content, settings, callback); break; case PDF: + //noinspection ConstantConditions printPdf(content, settings, callback); break; case HTML: - case SELF: case PLAIN: // TODO } @@ -121,15 +124,19 @@ class PrintManager { * @param settings Additional settings how to render the content. * @param callback The function to invoke once the job is done. */ - private void printPdf (String path, JSONObject settings, + private void printPdf (@NonNull String path, @NonNull JSONObject settings, @Nullable PrintHelper.OnPrintFinishCallback callback) { - PrintOptions options = new PrintOptions(settings); - String jobName = options.getJobName(); - PrintPdfAdapter adapter = new PrintPdfAdapter(path, context); - PrintAttributes attributes = options.toPrintAttributes(); + InputStream stream = PrintContent.open(path, context); - getPrintService().print(jobName, adapter, attributes); + if (stream == null) return; + + PrintOptions options = new PrintOptions(settings); + String jobName = options.getJobName(); + PrintAdapter adapter = new PrintAdapter(jobName, stream, callback); + PrintAttributes attrs = options.toPrintAttributes(); + + getPrintService().print(jobName, adapter, attrs); } /** @@ -139,11 +146,10 @@ class PrintManager { * @param settings Additional settings how to render the content. * @param callback The function to invoke once the job is done. */ - private void printImage (String path, JSONObject settings, + private void printImage (@NonNull String path, @NonNull JSONObject settings, @Nullable PrintHelper.OnPrintFinishCallback callback) { - AssetUtil decoder = new AssetUtil(context); - Bitmap bitmap = decoder.decode(path); + Bitmap bitmap = PrintContent.decode(path, context); if (bitmap == null) return; @@ -159,7 +165,8 @@ class PrintManager { /** * Returns the print service of the app. */ - private @NonNull android.print.PrintManager getPrintService() + @NonNull + private android.print.PrintManager getPrintService() { return (android.print.PrintManager) context.getSystemService(PRINT_SERVICE); } diff --git a/src/android/PrintOptions.java b/src/android/PrintOptions.java index 5895cad..25165eb 100644 --- a/src/android/PrintOptions.java +++ b/src/android/PrintOptions.java @@ -43,7 +43,7 @@ import static android.support.v4.print.PrintHelper.SCALE_MODE_FIT; class PrintOptions { // The print job settings - private @NonNull JSONObject spec; + private final @NonNull JSONObject spec; /** * Constructor @@ -140,9 +140,9 @@ class PrintOptions { break; } - if (spec.has("graystyle")) + if (spec.has("monochrome")) { - if (spec.optBoolean("graystyle")) + if (spec.optBoolean("monochrome")) { printer.setColorMode(PrintHelper.COLOR_MODE_MONOCHROME); } @@ -152,16 +152,13 @@ class PrintOptions { } } - if (spec.has("autoFit")) + if (spec.optBoolean("autoFit", true)) { - if (spec.optBoolean("autoFit")) - { - printer.setScaleMode(SCALE_MODE_FIT); - } - else - { - printer.setScaleMode(SCALE_MODE_FILL); - } + printer.setScaleMode(SCALE_MODE_FIT); + } + else + { + printer.setScaleMode(SCALE_MODE_FILL); } } } diff --git a/src/ios/APPPrinterInfo.m b/src/ios/APPPrinterInfo.m index 05eb3e0..1657038 100644 --- a/src/ios/APPPrinterInfo.m +++ b/src/ios/APPPrinterInfo.m @@ -38,7 +38,7 @@ info.orientation = UIPrintInfoOrientationLandscape; } - if ([spec[@"graystyle"] boolValue]) + if ([spec[@"monochrome"] boolValue]) { if ([spec[@"photo"] boolValue]) { diff --git a/src/windows/PrinterProxy.js b/src/windows/PrinterProxy.js index 725b1cd..62ad3a2 100644 --- a/src/windows/PrinterProxy.js +++ b/src/windows/PrinterProxy.js @@ -93,7 +93,7 @@ exports.onPrintTaskRequested = function (event) { args.setSource(exports._page); }); - if (config.graystyle) { + if (config.monochrome) { task.options.colorMode = Printing.PrintColorMode.grayscale; } else { task.options.colorMode = Printing.PrintColorMode.color; diff --git a/www/printer.js b/www/printer.js index c0f71ab..fd7fb13 100755 --- a/www/printer.js +++ b/www/printer.js @@ -23,12 +23,12 @@ var exec = require('cordova/exec'), // Defaults exports._defaults = { - // name: 'unknown', - // duplex: 'none', - // landscape: false, - // graystyle: false, - // border: true, - // copies: 1, + // name: 'unknown', + // duplex: 'none', + // landscape: false, + // monochrome: false, + // border: true, + // copies: 1, ui: { hideNumberOfCopies: false,