Save PDFs to a custom data provider on Android
Nutrient loads PDFs through objects that implement DataProvider. This interface supports sources such as cloud hosts, memory, and Android content providers.
Existing data provider classes
Nutrient provides these implementations of DataProvider:
AssetDataProviderloads PDFs from the app’sassets/directory, such as documents bundled in an APK. Copying assets to internal storage can perform better than reading them directly.ContentResolverDataProviderreads documents through Android’s content resolver. Acontent://URI identifies theContentProvider(opens in a new tab).InputStreamDataProvideris an abstract base class for reading from anInputStream(opens in a new tab). OverrideopenInputStream()to return a stream ready for reading.PDF reading requires random access, but an
InputStreamprovides sequential access.InputStreamDataProviderreopens the stream when it needs an earlier position, which can reduce performance. ForFileInputStream, it reads directly at the requested offset instead.AesDataProvider, included in the Catalog app, opens AES256-CTR-encrypted files without storing decrypted blocks. It supports random access to large PDFs without loading the entire file into memory.
Custom data provider
To create a custom provider, implement DataProvider and all its methods. For use with PdfActivity, also implement Android’s Parcelable(opens in a new tab) interface. Direct use with PdfFragment doesn’t require Parcelable.
The Catalog app’s CustomDataProviderExample reads a PDF from res/raw/ through an InputStream(opens in a new tab).
Writeable data providers
If you want your custom DataProvider to also be writeable, you need to implement the WritableDataProvider interface. This tells the framework that your DataProvider also supports writing changes made to the data. Here’s the outline of how this would appear:
class ExampleDataProvider : InputStreamDataProvider(), WritableDataProvider {
...
// Tells the system we can write to this data provider. override fun canWrite(): Boolean = true
override fun startWrite(writeMode: WritableDataProvider.WriteMode): Boolean { when (writeMode) { WritableDataProvider.WriteMode.REWRITE_FILE -> { // Prepare for writing, e.g. creating a new // temporary file to write to.
...
// Return `true` to indicate we can proceed with writing. return true } WritableDataProvider.WriteMode.APPEND_TO_FILE -> { // This won't occur when returning `false` in // `supportsAppending`. return false } } }
// This gets called repeatedly with the data we need to write. // Depending on the current write mode, either append // it to the existing data or write to a new file. override fun write(data: ByteArray): Boolean {
...
// Return `true` to indicate we can proceed with writing. return true }
// This is called once all data is written to give you an // opportunity to finish your writing process. override fun finishWrite(): Boolean {
...
// Return `true` to indicate writing was successful. return true }
// If you support appending data, you can return `true`. For this simple // example, we just return `false`. // Returning `true` doesn't mean it will always append; // you still need to support both write modes. override fun supportsAppending(): Boolean = false}class ExampleDataProvider extends InputStreamDataProvider implements WritableDataProvider {
...
@Override public boolean canWrite() { // Tells the system we can write to this data provider. return true; }
@Override public boolean startWrite(WriteMode writeMode) { switch (writeMode) { case REWRITE_FILE: // Prepare for writing, e.g. creating a new // temporary file to write to.
...
// Return `true` to indicate we can proceed with writing. return true; case APPEND_TO_FILE: // This won't occur when returning `false` in // `supportsAppending`. return false; }
return false; }
// This gets called repeatedly with the data we need to write. // Depending on the current write mode, either append // it to the existing data or write to a new file. @Override public boolean write(byte[] data) {
...
// Return `true` to indicate we can proceed with writing. return true; }
// This is called once all data is written to give you an // opportunity to finish your writing process. @Override public boolean finishWrite() {
...
// Return `true` to indicate writing was successful. return true; }
// If you support appending data you can return `true`. For this // simple example, we just return `false`. // Returning `true` doesn't mean it will always append; // you still need to support both write modes. @Override public boolean supportsAppending() { return false; }}If canWrite() returns false, Nutrient treats the document as read-only when it’s opened. Editing UI that would require saving back to that provider isn’t offered.
For a complete example, check out AesDataProvider, which is part of our Catalog app.
More uses for data providers
XfdfFormatter and DocumentJsonFormatter accept a DataProvider as input. To use the provider for output, implement WritableDataProvider and wrap it in OutputStreamAdapter.
The following example stores encrypted XFDF data:
val document = activity.requirePdfFragment().document ?: returnval file = ...val annotations = ...val formFields = ...// `AesDataProvider` is a sample data provider found in our Catalog app.val dataProvider = AesDataProvider(file.canonicalPath, BASE64_ENCRYPTION_KEY)
// Export all annotations in the document to our data provider.XfdfFormatter.writeXfdf(document, annotations, formFields, OutputStreamAdapter.Builder.fromDataProvider(dataProvider).build())
// You can use the same data provider for reimporting.val parsedAnnotations = XfdfFormatter.parseXfdf(document, dataProvider)File file = ...List<Annotation> annotations = ...List<FormField> formFields = ...// `AesDataProvider` is a sample data provider found in our Catalog app.AesDataProvider dataProvider = new AesDataProvider(file.getAbsolutePath(), BASE64_ENCRYPTION_KEY);
// Export all annotations in the document to our data provider.XfdfFormatter.writeXfdf(getDocument(), annotations, formFields, OutputStreamAdapter.Builder.fromDataProvider(dataProvider).build());
// You can use the same data provider for reimporting.List<Annotation> parsedAnnotations = XfdfFormatter.parseXfdf(getDocument(), dataProvider);Writing strategies
Choose a WritingStrategy for OutputStreamAdapter based on your provider’s requirements:
DirectWritingStrategywrites directly to theDataProvider. This is the default.TempFileWritingStrategywrites to a temporary file, and then commits all data to the provider. Use this strategy when an operation reads from and writes to the same provider.