You can use the Readium Kotlin toolkit to download and read publications that are protected with the Readium LCP DRM.
☝️ To use LCP with the Readium toolkit, you must first obtain the liblcp
private library by contacting EDRLab.
An LCP publication is protected with a user passphrase and distributed using an LCP License Document (.lcpl
) .
The user flow typically goes as follows:
- The user imports a
.lcpl
file into your application. - The application uses the Readium toolkit to download the protected publication from the
.lcpl
file to the user's bookshelf. The downloaded file can be a.epub
,.lcpdf
(PDF), or.lcpa
(audiobook) package. - The user opens the protected publication from the bookshelf.
- If the passphrase isn't already recorded in the
readium-lcp
internal database, the user will be asked to enter it to unlock the contents. - The publication is decrypted and rendered on the screen.
To support LCP in your application, you require two components:
- The
readium-lcp
module from the toolkit provides APIs for downloading and decrypting protected publications. Import it as you would any other Readium module, such asreadium-navigator
. - The private
liblcp
library customized for your application is available from EDRLab. They will provide instructions for integrating theliblcp
library into your application.
Readium LCP specifies new file formats.
Name | File extension | Media type |
---|---|---|
License Document | .lcpl |
application/vnd.readium.lcp.license.v1.0+json |
LCP for PDF package | .lcpdf |
application/pdf+lcp |
LCP for Audiobooks package | .lcpa |
application/audiobook+lcp |
☝️ EPUB files protected by LCP are supported without a special file extension or media type because EPUB accommodates any DRM scheme in its specification.
You may want to register these new file extensions and media types in the intent filters of your AndroidManifest.xml
.
readium-lcp
offers an LcpService
object that exposes its API. If liblcp
is not configured correctly in your application, the constructor will return null
. This is helpful if your application has build variants without LCP.
val lcpService = LcpService(
context = context,
assetRetriever = AssetRetriever(
contentResolver = context.contentResolver,
httpClient = DefaultHttpClient()
)
) ?: error("liblcp is missing on the classpath")))
Users need to import a License Document into your application to download the protected publication (.epub
, .lcpdf
, or .lcpa
).
The LcpService
offers an API to retrieve the full publication from an LCPL on the filesystem.
let acquisition = lcpService.acquirePublication(
lcpl = File("path/to/license.lcpl"),
onProgress = { progress ->
print(String.format("Downloaded %.1f%%", progress * 100)
}
).getOrElse { /* Failed to download the protected publication */ }
print("The publication was downloaded at ${acquisition.localFile}, its type is ${acquisition.format.mediaType}.")
After the download is completed, import the acquisition.localFile
file into the bookshelf like any other publication file.
If you want more control over the acquisition process, you can download the publication manually instead.
The acquisition is done in three steps:
- Parse the License Document (LCPL) file.
- Download the protected publication.
- Inject the LCPL into the downloaded package.
val lcplBytes: ByteArray = ...
val licenseDocument = LicenseDocument.fromBytes(lcplBytes)
.getOrElse { /* The LCPL appears to be invalid */ }
val publicationLink = licenseDocument.publicationLink
val downloadedFile = yourDownloadService.download(publicationLink.url())
.getOrElse { /* Failed to download the protected publication */ }
lcpService.injectLicenseDocument(licenseDocument, downloadedFile)
.getOrElse { /* Failed to inject the LCPL in the downloaded package */ }
// The downloaded file is now ready to be imported in your bookshelf as usual.
A publication protected with LCP can be opened using the PublicationOpener
component, just like a non-protected publication. However, you must provide a ContentProtection
implementation when initializing the PublicationOpener
to enable LCP. Luckily, LcpService
has you covered.
val authentication = LcpDialogAuthentication()
val publicationOpener = PublicationOpener(
publicationParser = DefaultPublicationParser(),
contentProtections = listOf(
lcpService.contentProtection(authentication)
)
)
An LCP package is secured with a user passphrase for decrypting the content. The LcpAuthenticating
interface expected by LcpService.contentProtection()
provides the passphrase when needed. You can use the default LcpDialogAuthentication
which displays a pop-up to enter the passphrase, or implement your own method for passphrase retrieval. If you already fetched the passphrase from a backend server, you can also use LcpPassphraseAuthentication(passphrase)
.
☝️ The user will be prompted once per passphrase since readium-lcp
stores known passphrases on the device.
For LcpDialogAuthentication
to function correctly, it needs to identify the host view displaying the dialog. You must indicate the host view, for example using a View.OnAttachStateChangeListener
in your bookshelf fragment.
class MyFragment : Fragment {
private inner class OnViewAttachedListener(
private val authentication: LcpDialogAuthentication
) : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(view: View) {
authentication.onParentViewAttachedToWindow(view)
}
override fun onViewDetachedFromWindow(view: View) {
authentication.onParentViewDetachedFromWindow()
}
}
private val onViewAttachedListener: OnViewAttachedListener = OnViewAttachedListener(
// Use your shared instance here.
lcpDialogAuthentication
)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.addOnAttachStateChangeListener(onViewAttachedListener)
}
}
You are now ready to open the publication file with your PublicationOpener
instance.
// Retrieve an `Asset` to access the file content.
val url = File("/path/to/lcp-protected-book.epub").toUrl()
val asset = assetRetriever.retrieve(url)
.getOrElse { /* Failed to retrieve the Asset */ }
// Open a `Publication` from the `Asset`.
val publication = publicationOpener.open(asset, allowUserInteraction = true)
.getOrElse { /* Failed to access or parse the publication */ }
The allowUserInteraction
argument is forwarded to the LcpAuthenticating
implementation when the passphrase is unknown. LcpDialogAuthentication
shows a pop-up only if allowUserInteraction
is true
.
When importing the publication to the bookshelf, set allowUserInteraction
to false
as you don't need the passphrase for accessing the publication metadata and cover. If you intend to present the publication using a Navigator, set allowUserInteraction
to true
as decryption will be required.
☝️ To check if a publication is protected with LCP before opening it, you can use asset.format.conformsTo(Specification.Lcp)
on the Asset
returned by the AssetRetriever
.
After obtaining a Publication
instance, you can access the publication's metadata to import it into the user's bookshelf. The user passphrase is not needed for reading the metadata or cover.
However, if you want to display the publication with a Navigator, verify it is not restricted. It could be restricted if the user passphrase is unknown or if the license is no longer valid (e.g., expired loan, revoked purchase, etc.).
if (publication.isRestricted) {
val error = publication.protectionError
if (error != null) {
// The user is not allowed to open the publication. You should display the error.
// In the case of LCP, `error` will be an `LcpError`.
} else {
// We don't have the user passphrase.
// You may use `publication` to access its metadata, but not to render its content.
}
} else {
// The publication is not restricted, you may render it with a Navigator component.
}
If the server hosting the LCP protected package supports HTTP Range requests, it is possible to stream directly an LCP protected publication from a License Document (.lcpl
) file, without downloading the whole publication first.
Simply open the License Document directly using the PublicationOpener
. Make sure you provide an HttpClient
(or an HttpResourceFactory
for additional customization) to the AssetRetriever
.
// Instantiate the required components.
val httpClient = DefaultHttpClient()
val assetRetriever = AssetRetriever(
contentResolver = context.contentResolver,
httpClient = httpClient
)
val publicationOpener = PublicationOpener(
publicationParser = DefaultPublicationParser(
context,
httpClient = httpClient,
assetRetriever = assetRetriever
)
)
// Retrieve an `Asset` to access the LCPL content.
val url = File("/path/to/license.lcpl").toUrl()
val asset = assetRetriever.retrieve(url)
.getOrElse { /* Failed to retrieve the Asset */ }
// Open a `Publication` from the LCPL `Asset`.
val publication = publicationOpener.open(asset, allowUserInteraction = true)
.getOrElse { /* Failed to access or parse the publication */ }
print("Opened ${publication.metadata.title}")
An LCP License Document contains metadata such as its expiration date, the remaining number of characters to copy and the user name. You can access this information using an LcpLicense
object.
Use the LcpService
to retrieve the LcpLicense
instance for a publication.
// Retrieve an `Asset` to access the file content.
val url = File("/path/to/lcp-protected-book.epub").toUrl()
val asset = assetRetriever.retrieve(url)
.getOrElse { /* Failed to retrieve the Asset */ }
if (!asset.format.conformsTo(Specification.Lcp)) {
// Not protected with LCP.
}
val lcpLicense = lcpService.retrieveLicense(
asset = asset,
authentication = authenticaton,
allowUserInteraction = true
).getOrElse { /* Failed to retrieve the LCP License from the publication */ }
lcpLicense.license.user.name?.let { name ->
print("The publication was acquired by $user")
}
lcpLicense.license.rights.end?.let { endDate ->
print("The loan expires on $endDate")
}
lcpLicense.charactersToCopyLeft?.let { copyLeft ->
print("You can copy up to $copyLeft characters remaining.")
}
☝️ If you have already opened a Publication
with the PublicationOpener
, you can directly obtain the LcpLicense
using publication.lcpLicense
.
Readium LCP allows borrowing publications for a specific period. Use the LcpLicense
object to manage a loan and retrieve its end date using lcpLicense.license.rights.end
.
Some loans can be returned before the end date. You can confirm this by using lcpLicense.canReturnPublication
. To return the publication, execute:
lcpLicense.returnPublication()
.onFailure { /* Failed to return the publication */ }
The loan end date may also be extended. You can confirm this by using lcpLicense.canRenewLoan
.
Readium LCP supports two types of renewal interactions:
- Programmatic: You show your own user interface.
- Interactive: You display a web view, and the Readium LSD server manages the renewal for you.
You need to support both interactions by implementing the LcpLicense.RenewListener
interface. A default Material Design implementation is available with MaterialRenewListener
.
val renewListener = MaterialRenewListener(
license = lcpLicense,
caller = hostFragment,
fragmentManager = hostFragment.childFragmentManager
)
lcpLicense.renewLoan(renewListener)
.onFailure { /* Failed to extend the loan end date */ }
The APIs may fail with an LcpError
. These errors must be displayed to the user with a suitable message.
For an example, take a look at LcpUserError.kt
in the Test App.