Refactor LCP Module #779
Replies: 2 comments 1 reply
-
|
Beta Was this translation helpful? Give feedback.
-
|
Thank you for opening this discussion, modernizing the LCP module is worth it. First of all, this code is critical, and we cannot afford any regressions. Before refactoring any significant part of the LCP service (especially LCP License Validation) we must introduce a comprehensive test suite for all LCP interactions and statuses. Unfortunately we cannot commit the LCP test setup to the repository, but we can use a build script and hidden GitHub repository secrets to initialize LCP on the CI. I already implemented this approach in the Swift toolkit. Test LCPL in various statuses can be generated from https://front-test.edrlab.org. Let me know if I can help you out designing this test suite. For reference, I think that this statechart is up to date: Now to each of your points. I think we agree with @qnga:
|
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
1. Summary
The
readium/lcpmodule suffers from severe threading anti-patterns, relies on outdated and unmaintained dependencies, duplicates networking logic, and enforces View-bound UI components that aren't easily adaptable by modern Jetpack Compose applications.This modernization effort will resolve critical concurrency hazards, significantly reduce the binary footprint, improve decryption performance, and improve developer experience for modern Android apps.
2. Critical Architectural Fixes (Coroutines & Concurrency)
The most urgent issues reside in how the LCP module manages Coroutines and state, leading to potential deadlocks and memory leaks.
2.1. The
runBlockingDeadlock Hazard inLicenseValidation.ktCurrently, the custom
StateMachineused for license validation processes state transitions and networking calls inside arunBlockingblock:This blocks the underlying thread while waiting for suspending network operations (
fetchStatus,fetchLicense). A comment inLicensesService.ktexplicitly notes that if this runs on the Main Dispatcher, it causes a severe deadlock with the UI when prompting for a passphrase.Actions:
StateMachineand therunBlockingwrapper.Replace it with a native Coroutine solution (e.g., aChannel-based actor or aMutableStateFlowloop) where state transitions and network calls are nativelysuspendfunctions. This eliminates the need for dangerous thread-shifting workarounds.2.2. Coroutine Scope Leak in
LicensesService.ktLicensesServiceimplementsCoroutineScopevia delegation toMainScope():While it launches coroutines within this scope, it never actually cancels it (it lacks a
close()oronDestroy()method). If a reading app drops its reference to theLcpServiceor instantiates a new one, theMainScopewill leak indefinitely along with any active background tasks.Actions:
CoroutineScope by MainScope()delegation.coroutineScope { }orsupervisorScope { }blocks) within the suspending functions.LicensesServicemust expose aclose()function to explicitly callcancel()on the scope when the host app shuts down the service.3. Dependency Cleanups
The module relies on several outdated libraries that bloat the application size and complicate maintenance.
3.1. Remove
com.mcxiaoke.koi:coreContext: This library is ancient and unmaintained. It is used exclusively for SHA-256 hashing and hex string conversions (e.g.,
HASH.sha256(clearPassphrase)).Actions:
readium/lcp/src/main/java/org/readium/r2/lcp/util/Digest.ktto supportStringandByteArrayusing standardjava.security.MessageDigest.ByteArray.toHexString().3.2. Remove
joda-timeContext:
joda-timeis obsolete and adds unnecessary binary size. It is primarily used inCRLService.ktfor calculating certificate expiration.Actions:
org.joda.time.DateTimewithorg.readium.r2.shared.util.Instant(which Readium uses to wrapkotlinx-datetime), or use standardjava.timeAPIs with desugaring.4. Network Stack Unification
Context: The LCP module implements its own networking via
NetworkService.kt, which manually wrapsHttpURLConnectionand managesBufferedInputStreamto download files and make REST calls.Actions:
NetworkService.kt.org.readium.r2.shared.util.http.HttpClientprovided by thereadium-sharedmodule.HttpClientto replace the customonProgressstream downloading logic.5. Native JNI & Performance Optimizations
Context: To make the native
liblcpbinary optional,LcpClient.ktrelies on reflection for every JNI call, including the highly repetitivedecryptfunction. This incurs a constant performance penalty during CBC stream decryption.Action:
LcpNativeProxy.LcpClient.isAvailable()is called.6. UI Modernization & Compose Compatibility
The current UI implementations aren't easily adaptable by apps built entirely in Jetpack Compose, as they hardcode legacy Android View dependencies.
6.1. Abstract
LcpDialogAuthentication.ktContext: This class is tightly coupled to Android XML layouts and
PopupWindow. Compose apps must provide a root anchorViewjust to show a dialog.Actions:
LcpAuthenticatingimplementation that exposes the authentication state as a state stream.6.2. Abstract
MaterialRenewListener.ktContext: Currently requires a
FragmentManagerto present a Material Date Picker for license renewal.Action: Provide a Compose-native alternative that uses Jetpack Compose date pickers, completely eliminating the need for
FragmentManager.7. API Modernization
7.1. Flow for License Acquisition
Context:
LcpService.acquirePublication()tracks progress via a closureonProgress: (Double) -> Unit.Actions:
Flow<AcquisitionStatus>.AcquisitionStatusshould be a sealed class representingLoading,Success, andFailure.8. JSON Parsing
Context: Every single data class in
readium/lcp/src/main/java/org/readium/r2/lcp/license/model/(e.g.,LicenseDocument,StatusDocument,Link,Rights, etc.) is manually parsing JSON usingorg.json.JSONObject. This is prone to runtime crashes, lacks type safety, and results in a lot of boilerplate code.Actions: Replace all
org.json.JSONObjectusage with Kotlin's@Serializable. This will improve parsing performance, ensure null-safety at compile-time, and delete boilerplate.9. ZIP Modification
Context: In
ContentZipLicenseContainer.kt, there is a performance problem. When the app needs to inject a downloaded LCP license into a file accessed via Android'sContentResolver, it currently copies the entire file to the device's cache directory, modifies the ZIP, and then copies the whole archive back over theContentResolverstream. If a user downloads a large LCP-protected book, injecting a 2KB license file will cause a massive I/O spike, doubling the storage required temporarily and freezing the app.Action: Modernize using a streaming ZIP manipulation approach, which allows updating ZIP entries without copying the entire payload to the local cache.
10. IDE Warnings
Context: There are a lot of warnings which somehow aren't stopped by
allWarningsAsErrors.Action: Clean up all warnings and weak warnings.
11. Tests
Context: There are only 3 files of tests for LCP.
Action: Add more before starting each piece of refactoring.
Beta Was this translation helpful? Give feedback.
All reactions