A problem with saving a PDF file on Android has been bugging me for weeks, making every Google search I do result in a list of purple links.
I have attempted numerous approaches, but have only managed to save a PDF file using the built in PDF creator, which does not fulfill my purpose.
I have a number of questions that I seek an answer to, and hope some of you can help.
Question 1: Where should you save a PDF file you intend to share and then delete immediately after?
Q1 a) Is a ContentResolver & ContentProvider required, or is this only when sharing a 'directory' between apps?
Intuitively it makes most sense for me to save the PDF in the internal cache directory, share it from there and then delete it.
Question 2: What is the approach for storing files with backwards compatibility?
From what I have read, different SDK levels require different approaches for file storage, being:
Using SAF (Storage Access Framework) seems to be the recommended way to store documents like PDFs. However, I don't intend to display a file picker to the user, when they are simply generating and sharing a PDF.
I am encountering this very generic exception, which occurs when iText closes the output stream. Below are some code examples and their resulting errors.
SDK 30 File API test
fun createPdfInternalSDK30(context: Context) {
val fileName = "test.pdf"
// MODE_PRIVATE creates in internal storage, right?
val out = context.openFileOutput(fileName, Context.MODE_PRIVATE)
// out path: /data/user/0/dk.overlevelsesguiden.de10her/files/test.pdf
try {
val writer = PdfWriter(out)
val pdf = PdfDocument(writer)
Document(pdf, PageSize.A4, false).apply {
add(Paragraph("Test"))
close()
}
} catch (e: IOException) {
e.printStackTrace()
}
}
Error
java.lang.ExceptionInInitializerError
at com.itextpdf.commons.actions.producer.ProducerBuilder.modifyProducer(ProducerBuilder.java:97)
at com.itextpdf.kernel.actions.events.FlushPdfDocumentEvent.doAction(FlushPdfDocumentEvent.java:103)
at com.itextpdf.commons.actions.EventManager.onEvent(EventManager.java:71)
at com.itextpdf.kernel.pdf.PdfDocument.close(PdfDocument.java:849)
at com.itextpdf.layout.Document.close(Document.java:117)
at dk.overlevelsesguiden.de10her.business.PDFService.createPdfInternalSDK30(PDFService.kt:118)
Caused by: java.util.regex.PatternSyntaxException: Syntax error in regexp pattern near index 12
\$\{([^}]*)}
SDK 29+ Scoped Storage test
@RequiresApi(api = Build.VERSION_CODES.Q)
fun createPdfSharedSDK29plus(context: Context) {
val pdfCollection = MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, "title")
put(MediaStore.MediaColumns.MIME_TYPE, "application/pdf")
}
var uri: Uri? = null
try {
uri = context.contentResolver.insert(pdfCollection, contentValues) ?: throw IOException("Failed to create new MediaStore record")
val out = context.contentResolver.openOutputStream(uri)
val writer = PdfWriter(out)
val pdf = PdfDocument(writer)
Document(pdf, PageSize.A4, false).apply {
add(Paragraph("Test"))
close()
}
} catch (e: IOException) {
uri?.let { orphanUri ->
context.contentResolver.delete(orphanUri, null, null)
}
}
}
Error
java.lang.ExceptionInInitializerError
at com.itextpdf.commons.actions.producer.ProducerBuilder.modifyProducer(ProducerBuilder.java:97)
at com.itextpdf.kernel.actions.events.FlushPdfDocumentEvent.doAction(FlushPdfDocumentEvent.java:103)
at com.itextpdf.commons.actions.EventManager.onEvent(EventManager.java:71)
at com.itextpdf.kernel.pdf.PdfDocument.close(PdfDocument.java:849)
at com.itextpdf.layout.Document.close(Document.java:117)
at dk.overlevelsesguiden.de10her.business.PDFService.finalTest(PDFService.kt:118)
Caused by: java.util.regex.PatternSyntaxException: Syntax error in regexp pattern near index 12
\$\{([^}]*)}
Manifest permissions
<!-- Without this folders will be inaccessible in Android-11 and above devices -->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<!-- Without this entry storage-permission entry will not be visible under app-info permissions list Android-10 and below -->
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28"
/>
You brought up a number of issues. This answer focuses on the actual errors you show.
The actual errors you show both cause a
java.util.regex.PatternSyntaxException: Syntax error in regexp pattern near index 12
\$\{([^}]*)}
during initialization of the iText class ProducerBuilder
. It happens here:
private static final String PATTERN_STRING = "\\$\\{([^}]*)}";
private static final Pattern PATTERN = Pattern.compile(PATTERN_STRING);
Actually here regular expressions in JREs differ from those in Android. In plain Java the regular expression \$\{([^}]*)}
is accepted, a }
meant as a character needs not to be escaped if there was no unescaped opening {
before. Android, on the other hand, requires it to be escaped even then.
A fix for this has been committed yesterday - https://github.com/itext/itext7/commit/32bf2552770866dce4516798e7b11f26631ae92f - the pattern string now is:
private static final String PATTERN_STRING = "\\$\\{([^}]*)\\}";
Thus, in the next iText release (or in a really current SNAPSHOT) that very error won't occur anymore.