Save PDFs to a custom data provider on Android
Nutrient supports loading data from many different sources. In fact, this can be done from any object that conforms to the DataProviding
protocol, which is known as a data provider.
A data provider defines a common interface for Nutrient to load PDF documents from arbitrary sources like cloud hosts, device RAM, content providers, and others.
Existing data provider classes
Nutrient comes with a range of predefined data providers that all implement DataProvider
:
-
AssetDataProvider
allows loading of documents directly from the app’sassets/
directory. This is useful if you ship PDF documents as part of your APK file. Note that copying assets to the internal device storage may perform better than reading them directly from the assets using this provider. -
ContentResolverDataProvider
uses Android’s content resolver framework for reading documents directly from aContentProvider
specified by a URI using thecontent://
scheme. -
InputStreamDataProvider
is an abstract base class that simplifies reading documents from anInputStream
. Subclasses have to override theopenInputStream()
method to provide the ready-to-read stream. Be aware that while it’s convenient to use anInputStream
, it can pose performance issues. This is caused by the fact that PDF documents are read using random access, whereasInputStream
only offers stream access. Therefore,InputStreamDataProvider
will reopen the underlying input stream every time it needs to “seek backward.” -
AesDataProvider
is shipped with the Catalog app and allows you to open AES256-CTR-encrypted files without storing the decrypted blocks anywhere. It supports random seeking and can handle large PDF files without causingOutOfMemoryExceptions
.
Custom data provider
To create a custom data provider for your application, you’ll have to create a class that implements the DataProvider
interface and all of its methods. If you’d like to use your data provider with PdfActivity
, your class also needs to implement Android’s Parcelable
interface. If you plan to use the data provider directly with the PdfFragment
, you don’t need to make it into a Parcelable.
Take a look at CustomDataProviderExample
inside the Catalog app. This shows how to create a data provider that can read a PDF document from the app’s res/raw/
directory using an InputStream
.
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; } }
For a complete example, check out AesDataProvider
, which is part of our Catalog app.
More uses for data providers
In certain cases, it can be beneficial to use a DataProvider
not just for displaying documents, but also to write data. Certain APIs, such as XfdfFormatter
and DocumentJsonFormatter
, already allow you to use a DataProvider
for the input. When using the OutputStreamAdapter
, you can also use the same DataProvider
for the output, so long as it implements WritableDataProvider
. Let’s look at an example of how to store your XFDF data encrypted:
val 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.canonicalPath, 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
The OutputStreamAdapter
can use a different WritingStrategy
depending on the requirements of the given DataProvider
. By default, we provide two WritingStrategy
implementations:
-
DirectWritingStrategy
— This writes immediately to theDataProvider
and is used by default. -
TempFileWritingStrategy
— This writes to a temporary file and only commits it to theDataProvider
once all data is ready. This is useful if the operation writing to yourDataProvider
is simultaneously reading from it.