Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions android/app/src/main/java/android/print/PdfPrintAdapterDriver.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package android.print;

import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;

/**
* Drives a PrintDocumentAdapter straight to a PDF file, without the system
* print dialog. Lives in the android.print package because the
* LayoutResultCallback / WriteResultCallback constructors are package-private:
* both classes are public SDK types on the greylist, and same-package
* compilation is the established way apps subclass them (the technique behind
* the common WebView-to-PDF converters). Runs on whichever thread the adapter
* expects — for WebView adapters that is the main thread.
*
* The caller owns the CancellationSignal: cancelling it aborts an in-flight
* layout or write, and the cancelled callbacks below still close the adapter
* lifecycle and report through the listener (the caller's completion guard
* drops duplicate deliveries). Synchronous adapter failures are reported the
* same way instead of escaping as uncaught exceptions.
*/
public final class PdfPrintAdapterDriver {

public interface Listener {
void onSuccess();
void onFailure(String message);
}

private PdfPrintAdapterDriver() {}

public static void print(
final PrintDocumentAdapter adapter,
PrintAttributes attributes,
final ParcelFileDescriptor destination,
final CancellationSignal cancellationSignal,
final Listener listener
) {
try {
adapter.onStart();
adapter.onLayout(
null,
attributes,
cancellationSignal,
new PrintDocumentAdapter.LayoutResultCallback() {
@Override
public void onLayoutFinished(PrintDocumentInfo info, boolean changed) {
try {
adapter.onWrite(
new PageRange[] { PageRange.ALL_PAGES },
destination,
cancellationSignal,
new PrintDocumentAdapter.WriteResultCallback() {
@Override
public void onWriteFinished(PageRange[] pages) {
finish(adapter);
listener.onSuccess();
}

@Override
public void onWriteFailed(CharSequence error) {
finish(adapter);
listener.onFailure(error == null ? "PDF write failed" : error.toString());
}

@Override
public void onWriteCancelled() {
finish(adapter);
listener.onFailure("PDF write cancelled");
}
}
);
} catch (RuntimeException ex) {
finish(adapter);
listener.onFailure("PDF write could not start: " + ex);
}
}

@Override
public void onLayoutFailed(CharSequence error) {
finish(adapter);
listener.onFailure(error == null ? "PDF layout failed" : error.toString());
}

@Override
public void onLayoutCancelled() {
finish(adapter);
listener.onFailure("PDF layout cancelled");
}
},
new Bundle()
);
} catch (RuntimeException ex) {
finish(adapter);
listener.onFailure("PDF print could not start: " + ex);
}
}

private static void finish(PrintDocumentAdapter adapter) {
try {
adapter.onFinish();
} catch (RuntimeException ignored) {
// The adapter may already be torn down with its WebView.
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,97 @@ public void shareMarkdownDocuments(PluginCall call) {
}
}

@PluginMethod
public void exportMarkdownPdf(PluginCall call) {
Activity activity = getActivity();
if (activity == null) {
call.reject("No Android activity is available for sharing", "SHARE_TARGET_UNAVAILABLE");
return;
}

String html = call.getString("html", null);
if (html == null || html.trim().length() == 0) {
call.reject("Rendered document content is required", "PDF_EXPORT_FAILED");
return;
}

String suggestedName = SharePreparation.normalizeSuggestedPdfName(call.getString("suggestedName", ""));
File outputFile;
try {
File exportDirectory = SharePreparation.getPdfExportCacheDirectory(getContext());
outputFile = new File(exportDirectory, suggestedName);
if (!SharePreparation.isFileInDirectory(outputFile, exportDirectory)) {
throw new SecurityException("PDF export path escaped the expected directory");
}
} catch (IOException | SecurityException ex) {
Log.e(TAG, "Failed to prepare PDF export directory", ex);
call.reject("Could not prepare the PDF file for sharing", "PDF_WRITE_FAILED", ex);
return;
}

final File pdfFile = outputFile;
final String displayName = suggestedName;
activity.runOnUiThread(() ->
PdfExporter.export(activity, html, displayName, pdfFile, new PdfExporter.Callback() {
@Override
public void onSuccess() {
sharePdfExport(call, activity, pdfFile, displayName);
}

@Override
public void onFailure(String code, String message) {
call.reject(message, code);
}
})
);
}

private void sharePdfExport(PluginCall call, Activity activity, File pdfFile, String displayName) {
try {
Uri pdfUri = FileProvider.getUriForFile(
getContext(),
getContext().getPackageName() + ".fileprovider",
pdfFile
);
Intent shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.setType("application/pdf");
shareIntent.putExtra(Intent.EXTRA_STREAM, pdfUri);
shareIntent.putExtra(Intent.EXTRA_TITLE, displayName);
shareIntent.putExtra(Intent.EXTRA_SUBJECT, displayName);
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
ArrayList<Uri> streamUris = new ArrayList<>();
streamUris.add(pdfUri);
shareIntent.setClipData(SharePreparation.buildShareClipData(getContext(), displayName, streamUris));

Intent chooser = Intent.createChooser(shareIntent, "Share PDF");
chooser.putExtra(
Intent.EXTRA_EXCLUDE_COMPONENTS,
new ComponentName[] { new ComponentName(getContext(), MainActivity.class) }
);
chooser.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
activity.startActivity(chooser);

JSObject result = new JSObject();
result.put("displayName", displayName);
result.put("mimeType", "application/pdf");
result.put("bytes", pdfFile.length());
Log.i(
TAG,
"Opened Android share sheet for exported PDF: " +
safeForLog(displayName) +
", bytes=" +
pdfFile.length()
);
call.resolve(result);
} catch (ActivityNotFoundException ex) {
Log.w(TAG, "No Android share target is available", ex);
call.reject("No Android share target is available", "SHARE_TARGET_UNAVAILABLE", ex);
} catch (RuntimeException ex) {
Log.e(TAG, "Failed to share exported PDF", ex);
call.reject("Could not prepare the PDF file for sharing", "PDF_WRITE_FAILED", ex);
}
}

@PluginMethod
public void renameMarkdownDocument(PluginCall call) {
String sourceUri = call.getString("sourceUri", "");
Expand Down
Loading
Loading