All migration steps necessary in reading apps to upgrade to major versions of the Kotlin Readium toolkit will be documented in this file.
Locator
objects between iOS and Android, you should wait for the 3.0 release of the Swift toolkit to upgrade your HREFs at the same time.
First of all, upgrade to version 2.4.3 and resolve any deprecation notices. This will help you avoid troubles, as the APIs that were deprecated in version 2.x have been removed in version 3.0.
If you integrate Readium 3.0 as a submodule, it requires Kotlin 1.9.24 and Gradle 8.6.0. You should start by updating these dependencies in your application.
The modules now target Android SDK 34. If your app also targets it, you will need the FOREGROUND_SERVICE_MEDIA_PLAYBACK
permission in your AndroidManifest.xml
file to use TTS and audiobook playback.
If you target Android devices running below API 26, you now must enable core library desugaring in your application module.
The Streamer
object has been deprecated in favor of components with smaller responsibilities:
AssetRetriever
grants access to the content of an asset located at a given URL, such as a publication package, manifest, or LCP licensePublicationOpener
uses a publication parser and a set of content protections to create aPublication
object from anAsset
.
See the user guide for a detailed explanation on how to use these new APIs.
The putPublication
and getPublication
helpers in Intent
are deprecated. Now, it is the application's responsibility to pass Publication
objects between activities and reopen them when necessary.
You can take a look at the ReaderRepository
in the Test App for inspiration.
Alternatively, you can copy the deprecated helpers and add them to your codebase. However, please note that this approach is discouraged because it will not handle configuration changes smoothly.
MediaType
no longer has static helpers for sniffing it from a file or URL. Instead, you can use an AssetRetriever
to retrieve the format of a file.
val httpClient = DefaultHttpClient()
val assetRetriever = AssetRetriever(context.contentResolver, httpClient)
val mediaType = assetRetriever.sniffFormat(File(...))
.getOrElse { /* Failed to access the asset or recognize its format */ }
.mediaType
Link.href
and Locator.href
are now respectively Href
and Url
objects. If you still need the string value, you can call toString()
, but you may find the Url
objects more useful in practice.
Use link.url()
to get a Url
from a Link
object.
Locator
objects.
In Readium v2.x, a Link
or Locator
's href
could be either:
- a valid absolute URL for a streamed publication, e.g.
https://domain.com/isbn/dir/my%20chapter.html
, - a percent-decoded path for a local archive such as an EPUB, e.g.
/dir/my chapter.html
.- Note that it was relative to the root of the archive (
/
).
- Note that it was relative to the root of the archive (
To improve the interoperability with other Readium toolkits (in particular the Readium Web Toolkits, which only work in a streaming context) Readium v3 now generates and expects valid URLs for Locator
and Link
's href
.
https://domain.com/isbn/dir/my%20chapter.html
is left unchanged, as it was already a valid URL./dir/my chapter.html
becomes the relative URL pathdir/my%20chapter.html
- We dropped the
/
prefix to avoid issues when resolving to a base URL. - Special characters are percent-encoded.
- We dropped the
You must migrate the HREFs or Locators stored in your database when upgrading to Readium 3. To assist you, two helpers are provided: Url.fromLegacyHref()
and Locator.fromLegacyJSON()
.
Here's an example of a Jetpack Room migration that can serve as inspiration:
val MIGRATION_HREF = object : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
val normalizedHrefs: Map<Long, String> = buildMap {
db.query("SELECT id, href FROM bookmarks").use { cursor ->
while (cursor.moveToNext()) {
val id = cursor.getLong(0)
val href = cursor.getString(1)
val normalizedHref = Url.fromLegacyHref(href)?.toString()
if (normalizedHref != null) {
put(id, normalizedHref)
}
}
}
}
val stmt = db.compileStatement("UPDATE bookmarks SET href = ? WHERE id = ?")
for ((id, href) in normalizedHrefs) {
stmt.bindString(1, href)
stmt.bindLong(2, id)
stmt.executeUpdateDelete()
}
}
}
Most APIs now return an Error
instance instead of an Exception
in case of failure, as these objects are not thrown by the toolkit but returned as values.
It is recommended to handle Error
objects using a when
statement. However, if you still need an Exception
, you may wrap an Error
with ErrorException
, for example:
assetRetriever.sniffFormat(...)
.getOrElse { throw ErrorException(it) }
UserException
is also deprecated. The application now needs to provide localized error messages for toolkit errors.
Clicking on external links is no longer managed by the EPUB navigator. To open the link yourself, override HyperlinkNavigator.Listener.onExternalLinkActivated
, for example:
override fun onExternalLinkActivated(url: AbsoluteUrl) {
if (!url.isHttp) return
val context = requireActivity()
val uri = url.toUri()
try {
CustomTabsIntent.Builder()
.build()
.launchUrl(context, uri)
} catch (e: ActivityNotFoundException) {
context.startActivity(Intent(Intent.ACTION_VIEW, uri))
}
}
Version 3 includes a new component called DirectionalNavigationAdapter
that replaces EdgeTapNavigation
. This helper enables users to navigate between pages using arrow and space keys on their keyboard or by tapping the edge of the screen.
As it implements InputListener
, you can attach it to any OverflowableNavigator
.
navigator.addInputListener(
DirectionalNavigationAdapter(
navigator,
animatedTransition = true
)
)
The DirectionalNavigationAdapter
provides plenty of customization options. Please refer to its API for more details.
The onTap
and onDrag
events of VisualNavigator.Listener
have been deprecated. You can now use multiple implementations of InputListener
. The order is important when events are consumed.
navigator.addInputListener(DirectionalNavigationAdapter(navigator))
navigator.addInputListener(object : InputListener {
override fun onTap(event: TapEvent): Boolean {
toggleUi()
return true
}
})
The EPUB navigator no longer displays a pop-up when the user activates a footnote link. This change was made to give reading apps control over the entire user interface.
The navigator now moves to the footnote content by default. To show your own pop-up instead, implement the new callback HyperlinkNavigator.Listener.shouldFollowInternalLink(Link, LinkContext?)
.
override fun shouldFollowInternalLink(
link: Link,
context: HyperlinkNavigator.LinkContext?
): Boolean =
when (context) {
is HyperlinkNavigator.FootnoteContext -> {
val text =
if (link.mediaType?.isHtml == true) {
Html.fromHtml(context.noteContent, Html.FROM_HTML_MODE_COMPACT)
} else {
context.noteContent
}
showPopup(text)
false
}
else -> true
}
The LcpService
now requires an instance of AssetRetriever
during construction.
val lcpService = LcpService(
context,
assetRetriever = assetRetriever
)
The way the host view of a LcpDialogAuthentication
is retrieved was changed to support Android configuration changes. You no longer need to pass an activity, fragment or view as sender
parameter.
Instead, call on your instance of LcpDialogAuthentication
:
onParentViewAttachedToWindow
every time you have a view attached to a window available as anchoronParentViewDetachedFromWindow
every time it gets detached
You can monitor these events by setting a View.OnAttachStateChangeListener
on your view. See the Test App for an example.
To reduce our depency to the JVM, we no longer use Date
objects in the toolkit. Instead, we added a custom Instant
type.
You can still translate from and to a Date
object with Instant.fromJavaDate()
and instant.toJavaDate()
.
Both the Fuel and Kovenant libraries have been completely removed from the toolkit. With that, several deprecated functions have also been removed.
To avoid conflicts when merging your app resources, all resources declared in the Readium toolkit now have the prefix readium_
. This means that you must rename any layouts or strings you have overridden. Some resources were removed from the toolkit.
If you referenced these resources, you need to remove them from your application or copy them to your own resources.
Name |
---|
colorPrimary |
colorPrimaryDark |
colorAccent |
colorAccentPrefs |
snackbar_background_color |
snackbar_text_color |
Name |
---|
end_of_chapter |
end_of_chapter_indicator |
zero |
epub_navigator_tag |
image_navigator_tag |
snackbar_text_color |
All the localized error messages are also removed.
If you used the resources listed below, you must rename the references to reflect the new names. You can use a global search to help you find the references in your project.
Deprecated | New |
---|---|
activity_r2_viewpager |
readium_navigator_viewpager |
fragment_fxllayout_double |
readium_navigator_fragment_fxllayout_double |
fragment_fxllayout_single |
readium_navigator_fragment_fxllayout_single |
popup_footnote |
readium_navigator_popup_footnote |
r2_lcp_auth_dialog |
readium_lcp_auth_dialog |
viewpager_fragment_cbz |
readium_navigator_viewpager_fragment_cbz |
viewpager_fragment_epub |
readium_navigator_viewpager_fragment_epub |
Deprecated | New |
---|---|
r2_navigator_epub_vertical_padding |
readium_navigator_epub_vertical_padding |
Deprecated | New |
---|---|
r2_lcp_dialog_cancel |
readium_lcp_dialog_cancel |
r2_lcp_dialog_continue |
readium_lcp_dialog_continue |
r2_lcp_dialog_forgotPassphrase |
readium_lcp_dialog_forgotPassphrase |
r2_lcp_dialog_help |
readium_lcp_dialog_help |
r2_lcp_dialog_prompt |
readium_lcp_dialog_prompt |
r2_lcp_dialog_reason_invalidPassphrase |
readium_lcp_dialog_reason_invalidPassphrase |
r2_lcp_dialog_reason_passphraseNotFound |
readium_lcp_dialog_reason_passphraseNotFound |
r2_lcp_dialog_support_mail |
readium_lcp_dialog_support_mail |
r2_lcp_dialog_support_phone |
readium_lcp_dialog_support_phone |
r2_lcp_dialog_support_web |
readium_lcp_dialog_support_web |
r2_media_notification_channel_description |
readium_media_notification_channel_description |
r2_media_notification_channel_name |
readium_media_notification_channel_name |
Deprecated | New |
---|---|
r2_media_notification_fastforward.xml |
readium_media_notification_fastforward.xml |
r2_media_notification_rewind.xml |
readium_media_notification_rewind.xml |
Readium is now distributed with Maven Central. You must update your Gradle configuration.
allprojects {
repositories {
- maven { url 'https://jitpack.io' }
+ mavenCentral()
}
}
The group ID of the Readium modules is now org.readium.kotlin-toolkit
, for instance:
dependencies {
implementation "org.readium.kotlin-toolkit:readium-shared:$readium_version"
implementation "org.readium.kotlin-toolkit:readium-streamer:$readium_version"
implementation "org.readium.kotlin-toolkit:readium-navigator:$readium_version"
implementation "org.readium.kotlin-toolkit:readium-opds:$readium_version"
implementation "org.readium.kotlin-toolkit:readium-lcp:$readium_version"
}
Decoration.extras
is now a Map<String, Any>
instead of Bundle
. You will need to update your app if you were storing custom data in extras
, for example:
val decoration = Decoration(...,
extras = mapOf("id" to id)
)
val id = decoration.extras["id"] as? Long
The PDF navigator got refactored to support arbitrary third-party PDF engines. As a consequence, PdfiumAndroid (the open source PDF renderer we previously used) was extracted into its own adapter package. This is a breaking change if you were supporting PDF in your application.
This new version ships with an adapter for the commercial PDF engine PSPDFKit, see the instructions under readium/adapter/pspdfkit
to set it up.
If you wish to keep using the open source library PdfiumAndroid, you need to migrate your app.
First, add the new dependency in your app's build.gradle
.
dependencies {
implementation "com.github.readium.kotlin-toolkit:readium-adapter-pdfium:$readium_version"
// Or, if you need only the parser but not the navigator:
implementation "com.github.readium.kotlin-toolkit:readium-adapter-pdfium-document:$readium_version"
}
Then, setup the Streamer
with the adapter factory:
Streamer(...,
pdfFactory = PdfiumDocumentFactory(context)
)
Finally, provide the new PdfiumEngineProvider
to PdfNavigatorFactory
:
val navigatorFactory = PdfNavigatorFactory(
publication = publication,
pdfEngineProvider = PdfiumEngineProvider()
)
override fun onCreate(savedInstanceState: Bundle?) {
childFragmentManager.fragmentFactory =
navigatorFactory.createFragmentFactory(...)
super.onCreate(savedInstanceState)
}
The local HTTP server is not needed anymore to render EPUB publications. You can safely drop all occurrences of Server
from your project and remove the baseUrl
parameter when calling EpubNavigatorFragment.createFactory()
.
If you were serving assets/
files (e.g. fonts or scripts) to the EPUB resources, you can still do so with the new API.
First, declare the assets/
paths that will be available to EPUB resources when creating the navigator. You can use simple glob patterns to allow multiple assets in one go, e.g. fonts/.*
.
EpubNavigatorFragment.createFactory(
...,
config = EpubNavigatorFragment.Configuration(
servedAssets = listOf(
"fonts/.*",
"annotation-icon.svg"
)
)
)
Then, use the base URL https://readium/assets/
to fetch your app assets from the web views. For example:
https://readium/assets/annotation-icon.svg
After removing the HTTP server, tapping on the edge of the screen will not turn pages anymore. If you wish to keep this behavior, you can add it in your app by implementing VisualNavigator.Listener.onTap()
. An instance of EdgeTapNavigation
can help to compute the page turns by taking into account the publication reading progression and custom thresholds. See an example in the test app.
override fun onTap(point: PointF): Boolean {
val navigated = EdgeTapNavigation(navigator).onTap(point, requireView())
if (!navigated) {
toggleAppBar()
}
return true
}
The 2.3.0 release introduces a brand new user preferences API for configuring the EPUB and PDF Navigators. This new API is easier and safer to use. To learn how to integrate it in your app, please refer to the user guide.
If you integrated the EPUB navigator from a previous version, follow these steps to migrate:
- Get familiar with the concepts of this new API.
- Remove the local HTTP server from your app, as explained in the previous section.
- Remove the whole
UserSettings.kt
file from your app, if you copied it from the Test App. - Adapt your user settings interface to the new API using preferences editors. The Test App and the user guide contain examples using Jetpack Compose.
- Handle the persistence of the user preferences. The settings are not stored in the
SharedPreferences
with nameorg.readium.r2.settings
anymore. Instead, you are responsible for persisting and restoring the user preferences as you see fit (e.g. as a JSON file).- If you want to migrate the legacy
SharedPreferences
settings, you can use the helperEpubPreferences.fromLegacyEpubSettings()
which will create a newEpubPreferences
object after translating the existing user settings.
- If you want to migrate the legacy
- Make sure you restore the stored user preferences when initializing the EPUB navigator.
Please refer to the following table for the correspondence between legacy settings and new ones.
Legacy | New |
---|---|
APPEARANCE_REF |
theme |
COLUMN_COUNT_REF |
columnCount (reflowable) and spread (fixed-layout) |
FONT_FAMILY_REF |
fontFamily |
FONT_OVERRIDE_REF |
N/A (handled automatically) |
FONT_SIZE_REF |
fontSize |
LETTER_SPACING_REF |
letterSpacing |
LINE_HEIGHT_REF |
lineHeight |
PAGE_MARGINS_REF |
pageMargins |
PUBLISHER_DEFAULT_REF |
publisherStyles |
reader_brightness |
N/A (out of scope for Readium) |
SCROLL_REF |
overflow (scrolled ) |
TEXT_ALIGNMENT_REF |
textAlign |
WORD_SPACING_REF |
wordSpacing |
publication.userSettingsUIPreset
is now deprecated, but you might still have this code in your application:
publication.userSettingsUIPreset[ReadiumCSSName.ref(SCROLL_REF)] = true
You can remove it, as the support for screen readers will be added directly to the navigator in a coming release. However if you want to keep it, here is the equivalent with the new API:
navigator.submitPreferences(currentPreferences.copy(scroll = true))
This hotfix release fixes an issue pulling a third-party dependency (NanoHTTPD) from JitPack.
After upgrading, make sure to remove the dependency to NanoHTTPD from your app's build.gradle
file before building:
-implementation("com.github.edrlab.nanohttpd:nanohttpd:master-SNAPSHOT") {
- exclude(group = "org.parboiled")
-}
-implementation("com.github.edrlab.nanohttpd:nanohttpd-nanolets:master-SNAPSHOT") {
- exclude(group = "org.parboiled")
-}
☝️ If you are stuck with an older version of Readium, you can use this workaround in your root build.gradle
, as an alternative.
With this new release, we migrated all the r2-*-kotlin
repositories to a single kotlin-toolkit
repository.
If you are integrating Readium with the JitPack Maven repository, the same Readium modules are available as before. Just replace the former dependency notations with the new ones, per the README.
dependencies {
implementation "com.github.readium.kotlin-toolkit:readium-shared:$readium_version"
implementation "com.github.readium.kotlin-toolkit:readium-streamer:$readium_version"
implementation "com.github.readium.kotlin-toolkit:readium-navigator:$readium_version"
implementation "com.github.readium.kotlin-toolkit:readium-opds:$readium_version"
implementation "com.github.readium.kotlin-toolkit:readium-lcp:$readium_version"
}
If you are integrating your own forks of the Readium modules, you will need to migrate them to a single fork and port your changes. Follow strictly the given steps and it should go painlessly.
- Upgrade your forks to the latest Readium 2.1.0 version from the legacy repositories, as you would with any update. The 2.1.0 version is available on both the legacy repositories and the new
kotlin-toolkit
one. It will be used to port your changes over to the single repository. - Fork the new
kotlin-toolkit
repository on your own GitHub space. - In a new local directory, clone your legacy forks as well as the new single fork:
mkdir readium-migration cd readium-migration # Clone the legacy forks git clone https://github.com/USERNAME/r2-shared-kotlin.git git clone https://github.com/USERNAME/r2-streamer-kotlin.git git clone https://github.com/USERNAME/r2-navigator-kotlin.git git clone https://github.com/USERNAME/r2-opds-kotlin.git git clone https://github.com/USERNAME/r2-lcp-kotlin.git # Clone the new single fork git clone https://github.com/USERNAME/kotlin-toolkit.git
- Reset the new fork to be in the same state as the 2.1.0 release.
cd kotlin-toolkit git reset --hard 2.1.0
- For each Readium module, port your changes over to the new fork.
rm -rf readium/*/src cp -r ../r2-shared-kotlin/r2-shared/src readium/shared cp -r ../r2-streamer-kotlin/r2-streamer/src readium/streamer cp -r ../r2-navigator-kotlin/r2-navigator/src readium/navigator cp -r ../r2-opds-kotlin/r2-opds/src readium/opds cp -r ../r2-lcp-kotlin/r2-lcp/src readium/lcp
- Review your changes, then commit.
git add readium git commit -m "Apply local changes to Readium"
- Finally, pull the changes to upgrade to the latest version of the fork. You might need to fix some conflicts.
git pull --rebase git push
Your fork is now ready! To integrate it in your app as a local Git clone or submodule, follow the instructions from the README.
Nothing to change in your app to upgrade from 2.0.0-beta.2 to the final 2.0.0 release! Please follow the relevant sections if you are upgrading from an older version.
This new beta is the last one before the final 2.0.0 release. It is mostly focused on bug fixes but we also adjusted the LCP and HTTP server APIs before setting it in stone for the 2.x versions.
The API used to serve Publication
resources with the Streamer's HTTP server was simplified. See the test app changes in PR #387.
Replace addEpub()
with addPublication()
, which does not expect the publication filename anymore. If the Publication
is servable, addPublication()
will return its base URL. This means that you do not need to:
- Call
Publication.localBaseUrlOf()
to get the base URL. Use the one returned byaddPublication()
instead. - Set the server port in the
$key-publicationPort
SharedPreferences
property.- If you copied the
R2ScreenReader
from the test app, you will need to update it to use directly the base URL instead of the$key-publicationPort
property. See this commit.
- If you copied the
R2EpubActivity
and R2AudiobookActivity
are expecting an additional Intent
extra: baseUrl
. Use the base URL returned by addPublication()
.
Find all the changes made in the test app related to LCP in PR #379.
We replaced all occurrences of Joda's DateTime
with java.util.Date
in r2-lcp-kotlin
, to reduce the dependency on third-party libraries. You will need to update any code using LcpLicense
. The easiest way would be to keep using Joda in your own app and create DateTime
object from the Date
ones. For example:
lcpLicense?.license?.issued?.let { DateTime(it) }
The API to renew an LCP loan got revamped to better support renewal through a web page. You will need to implement LcpLicense.RenewListener
to coordinate the UX interaction.
If your application fits Material Design guidelines, you may use the provided MaterialRenewListener
implementation directly. This will only work if your theme extends a MaterialComponents
one, for example:
<style name="AppTheme" parent="Theme.MaterialComponents.Light.DarkActionBar">
MaterialRenewListener
expects an ActivityResultCaller
instance for argument. Any ComponentActivity
or Fragment
object can be used as ActivityResultCaller
.
val activity: FragmentActivity
license.renewLoan(MaterialRenewListener(
license = lcpLicense,
caller = activity,
fragmentManager = activity.supportFragmentManager
))
The version 2.0.0-beta.1 is mostly stabilizing the new APIs and fixing existing bugs. We also upgraded the libraries to be compatible with Kotlin 1.4 and Gradle 4.1.
To simplify the new format API, we merged Format
into MediaType
to offer a single interface. If you were using Format
, you should be able to replace it by MediaType
seamlessly.
Streamer.open()
is now expecting an implementation of PublicationAsset
instead of an instance of File
. This allows to open publications which are not represented as files on the device. For example a stream, an URL or any other custom structure.
Readium ships with a default implementation named FileAsset
replacing the previous File
type. The API is the same so you can just replace File
by FileAsset
in your project.
This new version is now compatible with display cutouts. However, this is an opt-in feature. To support display cutouts, follow these instructions:
- IMPORTANT: You need to remove any
setPadding()
statement from your app inUserSettings.kt
, if you copied it from the test app. - If you embed a navigator fragment (e.g.
EpubNavigatorFragment
) yourself, you need to opt-in by specifying thelayoutInDisplayCutoutMode
of the hostActivity
. R2EpubActivity
andR2CbzActivity
automatically applyLAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
to their window'slayoutInDisplayCutoutMode
.PdfNavigatorFragment
is not yet compatible with display cutouts, because of limitations from the underlying PDF viewer.
The 2.0.0 introduces numerous new APIs in the Shared Models, Streamer and LCP libraries, which are detailed in the following proposals. We highly recommend skimming over the "Developer Guide" section of each proposal before upgrading to this new major version.
- Format API
- Composite Fetcher API
- Publication Encapsulation
- Publication Helpers and Services
- Streamer API
- Content Protection
This r2-testapp-kotlin
commit showcases all the changes required to upgrade the Test App.
Please reach out on Slack if you have any issue migrating your app to Readium 2.0.0, after checking the troubleshooting section.
A new Streamer
class deprecates the use of individual PublicationParser
implementations, which you will need to replace in your app.
Call Streamer::open()
to parse a publication. It will return a self-contained Publication
model which handles metadata, resource access and DRM decryption. This means that Container
, PubBox
and DRM
are not needed anymore, you can remove any reference from your app.
The allowUserInteraction
parameter should be set to true
if you intend to render the parsed publication to the user. It will allow the Streamer to display interactive dialogs, for example to enter DRM credentials. You can set it to false
if you're parsing a publication in a background process, for example during bulk import.
val streamer = Streamer(context)
val publication = streamer.open(File(path), allowUserInteraction = true)
.getOrElse { error ->
alert(error.getUserMessage(context))
return
}
You can't use Publication.fromJSON()
to parse directly a manifest anymore. Instead, you can use Manifest.fromJSON()
, which gives you access to the metadata embedded in the manifest.
Then, if you really need a Publication
model, you can build one yourself from the Manifest
and optionally a Fetcher
and Publication Services.
-val publication = Publication.fromJSON(json)
+val publication = Manifest.fromJSON(json)?.let { Publication(it) }
However, the best way to parse a RWPM is to use the Streamer
, like with any other publication format. This way the Publication
model will be initialized with appropriate Fetcher
and Publication Services.
In case of failure, a Publication.OpeningException
is returned. It implements UserException
and can be used directly to present an error message to the user with getUserMessage(Context)
.
If you wish to customize the error messages or add translations, you can override the strings declared in r2-shared-kotlin/r2-shared/src/main/res/values/strings.xml
in your own app module. This goes for LCP errors as well, which are declared in r2-lcp-kotlin/r2-lcp/src/main/res/values/strings.xml
.
Streamer
offers other useful APIs to extend the capabilities of the Readium toolkit. Take a look at its documentation for more details, but here's an overview:
- Add new custom parsers.
- Integrated DRM support, such as LCP.
- Provide different implementations for third-party tools, e.g. ZIP, PDF and XML.
- Customize the
Publication
's metadata orFetcher
upon creation. - Collect authoring warnings from parsers.
Since the new Publication
model is self-contained, you can replace any use of the Container
API by publication.get(Link)
. This works for any publication format supported by the Streamer
's parsers.
The test app used to have special cases for DiViNa and Audiobooks, by unpacking manually the ZIP archives. You should remove this code and streamline any resource access using publication.get()
.
Extracting the cover of a publication for caching purposes can be done with a single call to publication.cover()
, instead of reaching for a Link
with cover
relation. You can use publication.coverFitting(Size)
to select the best resolution without exceeding a given size. It can be useful to avoid saving very large cover images.
-val cover =
- try {
- publication.coverLink
- ?.let { container.data(it.href) }
- ?.let { BitmapFactory.decodeByteArray(it, 0, it.size) }
- } catch (e: Exception) {
- null
- }
+val cover = publication.coverFitting(Size(width = 100, height = 100))
Navigator::currentLocator
is now a StateFlow
instead of LiveData
, to better support chromeless navigators such as an audiobook navigator in the future.
If you were observing currentLocator
from an Activity
or Fragment
, you can continue to do so with currentLocator.asLiveData()
.
- navigator.currentLocator.observe(this, Observer { locator -> })
+ navigator.currentLocator.asLiveData().observe(this, Observer { locator -> })
If you access directly the value through navigator.currentLocator.value
, you might need to add the following annotation to the enclosing class:
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
Despite being still experimental, StateFlow
is deemed stable for use.
Support for LCP is now fully integrated with the Streamer
, which means that you don't need to retrieve the LCP license and fill container.drm
yourself after opening a Publication
anymore.
To enable the support for LCP in the Streamer
, you need to initialize it with a ContentProtection
implementation provided by r2-lcp-kotlin
.
val lcpService = LcpService(context)
val streamer = Streamer(
context = context,
contentProtections = listOfNotNull(
lcpService?.contentProtection()
)
)
Then, to prompt the user for their passphrase, you need to set allowUserInteraction
to true
and provide the instance of the hosting Activity
, Fragment
or View
with the sender
parameter when opening the publication.
streamer.open(File(path), allowUserInteraction = true, sender = activity)
Alternatively, if you already have the passphrase, you can pass it directly to the credentials
parameter. If it's valid, the user won't be prompted.
The LCP Service now ships with a default passphrase dialog. You can remove the former implementation from your app if you copied it from the test app. But if you still want to use a custom implementation of LcpAuthenticating
, for example to have a different layout, you can pass it when creating the ContentProtection
.
lcpService.contentProtection(CustomLCPAuthentication())
In case the credentials were incorrect or missing, the Streamer
will still return a Publication
, but in a "restricted" state. This allows reading apps to import publications by accessing their metadata without having the passphrase.
But if you need to present the publication with a Navigator, you will need to first check if the Publication
is not restricted.
Besides missing credentials, a publication can be restricted if the Content Protection returned an error, for example when the publication is expired. In which case, you must display the error to the user by checking the presence of a publication.protectionError
.
if (publication.isRestricted) {
publication.protectionError?.let { error ->
// A status error occurred, for example the publication expired
alert(error.getUserMessage(context))
}
} else {
presentNavigator(publication)
}
To check if a publication is protected with a known DRM, you can use publication.isProtected
.
If you need to access an LCP license's information, you can use the helper publication.lcpLicense
, which will return the LcpLicense
if the publication is protected with LCP and the passphrase was known. Alternatively, you can use LcpService::retrieveLicense()
as before.
LcpService.importPublication()
was replaced with acquirePublication()
, which is a cancellable suspending function. It doesn't require the user to enter its passphrase anymore to download the publication.
You can integrate additional DRMs, such as Adobe ACS, by implementing the ContentProtection
protocol. This will provide first-class support for this DRM in the Streamer and Navigator.
Take a look at the Content Protection proposal for more details. An example implementation can be found in r2-lcp-kotlin
.
A few of the new APIs are returning a Try
object, which is similar to the native Result
type. We decided to go for this opiniated approach for error handling instead of throwing Exception
because of the type-safety it brings and the constraint on reading apps to properly handle error cases.
You can revert to traditional exceptions by calling getOrThrow()
on the Try
instance, but the most convenient way to handle the error would be to use getOrElse()
.
val publication = streamer.open(File(path), allowUserInteraction = true)
.getOrElse { error ->
alert(error.getUserMessage(context))
return
}
Try
also supports map()
and flatMap()
which are useful to transform the result while forwarding any error handling to upper layers.
fun cover(): Try<Bitmap, ResourceException> =
publication.get(coverLink)
.use { resource -> resource.read() } // <- returns a Try<ByteArray, ResourceException>
.map { bytes -> BitmapFactory.decodeByteArray(bytes, 0, bytes.size) }
Attempt to invoke virtual method 'android.content.SharedPreferences android.content.Context.getSharedPreferences(java.lang.String, int)
' on a null object reference
Make sure you create the LcpService
after onCreate()
has been called on an Activity
.
Make sure you added the following to your app's build.gradle
:
implementation "readium:liblcp:1.0.0@aar"
Make sure you added the content protection to the Streamer, following these instructions.
E/LcpDialogAuthentication: No valid [sender] was passed to LcpDialogAuthentication::retrievePassphrase()
. Make sure it is an Activity, a Fragment or a View.
To be able to present the LCP passphrase dialog, the default LcpDialogAuthentication
needs a hosting view as context. You must provide it to the sender
parameter of Streamer::open()
.
streamer.open(File(path), allowUserInteraction = true, sender = activity)
IllegalArgumentException: The provided publication is restricted. Check that any DRM was properly unlocked using a Content Protection.
Navigators will refuse to be opened if a publication is protected and not unlocked. You must check if a publication is not restricted by following these instructions.
With this new release, we started a process of modernization of the Readium Kotlin toolkit to:
- better follow Android best practices and Kotlin conventions,
- reduce coupling between reading apps and Readium, to ease future migrations and allow refactoring of private core implementations,
- increase code safety,
- unify Readium APIs across platforms through public specifications.
As such, this release will break existing codebases. While most changes are facilitated thanks to deprecation warnings with automatic fixes, there are a few changes listed below that you will need to operate manually.
- The
Publication
shared models were moved to their own package. While there are deprecated aliases helping with migration, it doesn't work forPublication.EXTENSION
. Therefore, you need to replace all occurrences oforg.readium.r2.shared.Publication
byorg.readium.r2.shared.publication.Publication
in your codebase.- Or better, don't use
Publication.EXTENSION
anymore. We have a newFormat
API which handles these needs in a more systematic way. And instead of file extensions, we recommend to store media types in your database.
- Or better, don't use
- A few
Publication
andLink
properties, such asimages
,pageList
andnumberOfItems
were moved to a different package. Simply trigger the "Import" feature of your IDE to resolve them.
The Publication
shared models are now immutable to increase code safety. This should not impact reading apps much unless you were creating Publication
or other models yourself.
However, there are a few places in the Test App that needs to be updated:
Publication
'sreadingOrder
,links
, andtableOfContents
are notMutableList
anymore, but read-onlyList
. Therefore, you need to update any code expecting mutable lists.Locator
can't be modified directly anymore. Instead, use thecopy()
orcopyWithLocations()
Locator
APIs.
Best practices on observing and restoring the last location were updated in the Test App, and it is highly recommended that you update your codebase as well, to avoid any issues.
You need to make these changes in your implementations of EpubActivity
, ComicActivity
and AudiobookActivity
:
- Remove any overrides of
currentLocation
. - Restore the last location from your database in
onCreate()
, for example with something similar to:
// Restores the last read location
bookRepository.lastLocatorOfBook(bookId)?.let { locator ->
go(locator, animated = false)
}
NavigatorDelegate.locationDidChange()
is now deprecated in favor of the more idiomatic Navigator.currentLocator: LiveData<Locator?>
.
currentLocator.observe(this, Observer { locator ->
if (locator != null) {
bookRepository.saveLastLocatorOfBook(bookId, locator)
}
})
- A new Positions List feature was added to provide a list of discrete locations in a publication. It can be used to implement an approximation of page numbers. This replaces the existing
R2SyntheticPageList
, which should be removed from your codebase. - Publications parsed from large manifests could crash the application when starting a reading activity. To fix this,
Publication
must not be put in anIntent
extra anymore. Instead, use the newIntent
extensions provided by Readium. This solution is a crutch until we move away fromActivity
in the Navigator and let reading apps handle the lifecycle ofPublication
themselves.- Replace all occurrences of
putExtra("publication", publication)
or similar byputPublication(publication)
. - Replace all occurrences of
getSerializableExtra("publication")
or similar bygetPublication(this)
.
- Replace all occurrences of
Locator
is nowParcelable
instead ofSerializable
, you must replace all occurrences ofgetSerializableExtra("locator")
bygetParcelableExtra("locator")
.Locations.fragment
was renamed tofragments
, and is now aList
. You need to update your code if you were creatingLocations
yourself.locations
andtext
are not nullable anymore.Locator
's constructor has a default value, so you don't need to passnull
for them anymore.Locator
is not meant to be subclassed, and extending it is not possible anymore. If your project is based on the Test App, you need to do the following changes in your codebase:- Don't extend
Locator
inBookmark
andHighlight
. Instead, add alocator
property which will create aLocator
object from their properties. Then, in places where you were creating aLocator
from a database model, you can use this property directly. - For
SearchLocator
, you have two choices:- (Recommended) Replace all occurrences of
SearchLocator
byLocator
. These two models are interchangeable. - Use the same strategy described above for
Bookmark
.
- (Recommended) Replace all occurrences of
- Don't extend
class Bookmark(...) {
val locator get() = Locator(
href = resourceHref,
type = resourceType,
title = resourceTitle,
locations = location,
text = locatorText
)
}
The CSS, JavaScript and fonts injection in the Server
was refactored to reduce the risk of collisions and simplify your codebase. This is a breaking change, to upgrade your app you need to:
- Provide the application's
Context
when creating aServer
. - Remove the following injection statements, which are now handled directly by the Streamer:
server.loadCustomResource(assets.open("scripts/crypto-sha256.js"), "crypto-sha256.js", Injectable.Script)
server.loadCustomResource(assets.open("scripts/highlight.js"), "highlight.js", Injectable.Script)