Enforce the LCP copy allowance in the navigators - #826
Draft
mickael-menu wants to merge 7 commits into
Draft
mickael-menu wants to merge 7 commits into
mickael-menu wants to merge 7 commits into
Conversation
The allowance checks only allowed over-budget copies/prints. Also log allowance-consumption failures at error level in all builds, so transient database errors are diagnosable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ener Groundwork for counted copy enforcement in the navigators: a single CopyError type shared by legacy and new navigators, an internal atomic (non-cancellable, main-thread) helper consuming the copy allowance before writing the clipboard, and a default no-op Navigator.Listener.onCopyForbidden() callback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces the user-select:none hack on protected publications with real copy enforcement: DOM copy events (system selection menu Copy, Ctrl+C) are intercepted in the JS layer and routed through a counted native copy consuming the Content Protection's copy right. Adds a stable EpubNavigatorFragment.copySelection() for apps with a custom selection ActionMode callback, and deprecates disableSelectionWhenProtected, which is now ignored. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces the user-select:none hack with proper copy-allowance enforcement in the Compose reflowable and fixed-layout renditions: a JS CopyInterceptor forwards intercepted copy events (system selection menu Copy, Ctrl+C) to native code performing a counted copy, SelectionController.copySelection() performs counted programmatic copies, and the new CopyListener reports forbidden copies to the app. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces disableCopyPaste() on protected publications with an interception of the built-in Copy popup toolbar item, which consumes the Content Protection's copy right before writing to the clipboard. Speak and Copy remain available; Share is still stripped. On denial, Navigator.Listener.onCopyForbidden() is notified. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both apps replace the system selection menu with a custom ActionMode callback, which loses the system Copy item. They now add their own Copy item routed through the navigator's copySelection(), which enforces the copy allowance of protected publications, and surface forbidden copies with a snackbar (test app) or toast (demo) also used for copies intercepted by the navigator. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds CHANGELOG entries with the compliance warning, a migration guide section with the required copySelection() pattern for custom selection ActionMode callbacks, a Copy protection section in the LCP guide, a CONTEXT.md domain glossary, and unit tests for the canCopy/canPrint boundary fix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR updates the Kotlin navigators to enforce LCP (and other Content Protection) copy allowances by intercepting copy operations (system menu / Ctrl+C) and routing them through counted native copies, instead of disabling selection wholesale.
Changes:
- Introduces a shared
CopyErrormodel and acopyToClipboard()helper which consumes allowance before writing to the clipboard (with optional clearing on denial for interception paths). - Adds navigator-level copy interception for legacy EPUB WebView, new Compose web renditions (reflowable + fixed layout), and PSPDFKit; adds app/demo wiring to handle “copy forbidden” feedback.
- Fixes inverted LCP allowance comparisons in
canCopy()/canPrint()and adds unit tests; updates docs/migration guidance and changelog.
Reviewed changes
Copilot reviewed 43 out of 49 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| test-app/src/main/res/values/strings.xml | Adds UI strings for Copy action and “copy forbidden” feedback. |
| test-app/src/main/res/menu/menu_action_mode.xml | Adds a Copy item to the selection ActionMode menu. |
| test-app/src/main/java/org/readium/r2/testapp/reader/VisualReaderFragment.kt | Routes custom ActionMode Copy through EpubNavigatorFragment.copySelection(). |
| test-app/src/main/java/org/readium/r2/testapp/reader/ReaderViewModel.kt | Emits a CopyForbidden command for UI feedback. |
| test-app/src/main/java/org/readium/r2/testapp/reader/ReaderActivity.kt | Shows a Snackbar when copy is forbidden. |
| readium/shared/src/main/java/org/readium/r2/shared/publication/services/CopyError.kt | Adds CopyError and UserRights.copyToClipboard() helper. |
| readium/navigators/web/reflowable/src/main/kotlin/org/readium/navigator/web/reflowable/resource/ReflowableResource.kt | Wires WebView JS copy interception into reflowable resources. |
| readium/navigators/web/reflowable/src/main/kotlin/org/readium/navigator/web/reflowable/ReflowableWebRenditionState.kt | Adds protection state + counted-copy plumbing for reflowable web rendition. |
| readium/navigators/web/reflowable/src/main/kotlin/org/readium/navigator/web/reflowable/ReflowableWebRenditionFactory.kt | Passes rights/isProtected into reflowable rendition state. |
| readium/navigators/web/reflowable/src/main/kotlin/org/readium/navigator/web/reflowable/ReflowableWebRendition.kt | Adds CopyListener and dispatches forbidden-copy notifications for intercepted copies. |
| readium/navigators/web/reflowable/src/main/kotlin/org/readium/navigator/web/reflowable/injection/ReflowableHtmlInjector.kt | Removes the “disable selection” CSS injection hack. |
| readium/navigators/web/internals/src/main/kotlin/org/readium/navigator/web/internals/webapi/CopyApi.kt | Adds native JS-bridge API to gate/intercept copy events. |
| readium/navigators/web/internals/src/main/kotlin/org/readium/navigator/web/internals/util/HtmlInjector.kt | Removes the legacy disableSelectionInjectable constant. |
| readium/navigators/web/internals/src/main/assets/readium/navigator/web/internals/generated/fixed-injectable-script.js | Updates generated fixed-layout injectable script (now includes copy interception). |
| readium/navigators/web/internals/scripts/src/index-reflowable-injectable.ts | Installs CopyInterceptor + SelectionReporter in reflowable injectable script. |
| readium/navigators/web/internals/scripts/src/index-fixed-injectable.ts | Installs CopyInterceptor + SelectionReporter in fixed-layout injectable script. |
| readium/navigators/web/internals/scripts/src/common/copy.ts | Adds JS-side CopyInterceptor and listener bridge types. |
| readium/navigators/web/fixedlayout/src/main/kotlin/org/readium/navigator/web/fixedlayout/spread/SingleViewportSpread.kt | Wires copy interception into the single-viewport fixed-layout WebView. |
| readium/navigators/web/fixedlayout/src/main/kotlin/org/readium/navigator/web/fixedlayout/spread/DoubleViewportSpread.kt | Wires copy interception into the double-viewport fixed-layout WebView. |
| readium/navigators/web/fixedlayout/src/main/kotlin/org/readium/navigator/web/fixedlayout/injection/FixedHtmlInjector.kt | Removes the “disable selection” CSS injection hack for fixed layout. |
| readium/navigators/web/fixedlayout/src/main/kotlin/org/readium/navigator/web/fixedlayout/FixedWebRenditionState.kt | Adds rights/protection + counted-copy plumbing for fixed-layout web rendition. |
| readium/navigators/web/fixedlayout/src/main/kotlin/org/readium/navigator/web/fixedlayout/FixedWebRenditionFactory.kt | Passes rights/isProtected into fixed-layout rendition state. |
| readium/navigators/web/fixedlayout/src/main/kotlin/org/readium/navigator/web/fixedlayout/FixedWebRendition.kt | Adds CopyListener and dispatches forbidden-copy notifications for intercepted copies. |
| readium/navigators/common/src/main/java/org/readium/navigator/common/SelectionController.kt | Adds copySelection() API for counted programmatic copies. |
| readium/navigators/common/src/main/java/org/readium/navigator/common/CopyListener.kt | Adds CopyListener and NullCopyListener for intercepted-copy denial notifications. |
| readium/navigator/src/main/java/org/readium/r2/navigator/R2BasicWebView.kt | Adds JS-bridge gate + callback for copy interception. |
| readium/navigator/src/main/java/org/readium/r2/navigator/Navigator.kt | Adds Navigator.Listener.onCopyForbidden() callback. |
| readium/navigator/src/main/java/org/readium/r2/navigator/epub/WebViewServer.kt | Removes disableSelectionWhenProtected plumbing. |
| readium/navigator/src/main/java/org/readium/r2/navigator/epub/HtmlInjector.kt | Removes “disable selection” injection in legacy EPUB navigator. |
| readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorViewModel.kt | Adds counted-copy helpers for intercepted and programmatic copy paths. |
| readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorFragment.kt | Adds copySelection() and hooks copy interception to listener callbacks. |
| readium/navigator/src/main/assets/_scripts/src/selection.js | Intercepts DOM copy event and forwards selected text to native. |
| readium/lcp/src/test/java/org/readium/r2/lcp/license/LicenseTest.kt | Adds unit tests validating canCopy() / canPrint() boundaries. |
| readium/lcp/src/test/java/org/readium/r2/lcp/fakes/FakeLcpDao.kt | Makes FakeLcpDao open for test customization. |
| readium/lcp/src/main/java/org/readium/r2/lcp/license/License.kt | Fixes inverted comparisons and ensures allowance-consumption failures are logged. |
| readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitEngineProvider.kt | Wires onCopyForbidden() through PSPDFKit engine callbacks. |
| readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitDocumentFragment.kt | Intercepts PSPDFKit Copy item to perform counted copy; removes disableCopyPaste(). |
| docs/migration-guide.md | Documents the new copy enforcement model and required app changes. |
| docs/guides/lcp.md | Documents copy allowance behavior and onCopyForbidden() guidance. |
| demos/navigator/src/main/res/menu/menu_action_mode.xml | Adds Copy item to the demo ActionMode menu. |
| demos/navigator/src/main/java/org/readium/demo/navigator/reader/Rendition.kt | Wires CopyListener and shows user feedback on forbidden copies. |
| demos/navigator/src/main/java/org/readium/demo/navigator/reader/ActionMode.kt | Routes custom ActionMode Copy through SelectionController.copySelection(). |
| CONTEXT.md | Adds domain glossary for copy protection concepts. |
| CHANGELOG.md | Adds changelog entries for copy enforcement + LCP fixes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+121
to
136
| var copyListenerApi by remember(webViewState.webView) { | ||
| mutableStateOf<CopyListenerApi?>(null) | ||
| } | ||
|
|
||
| val onCopyInterceptedRef by rememberUpdatedRef(onCopyIntercepted) | ||
|
|
||
| LaunchedEffect(webViewState.webView) { | ||
| webViewState.webView?.let { webView -> | ||
| gesturesApi = GesturesApi(webView) | ||
| documentStateApi = DocumentStateApi(webView) | ||
| selectionListenerApi = SelectionListenerApi(webView) | ||
| copyListenerApi = CopyListenerApi(webView, interceptEnabled = interceptCopy) { | ||
| onCopyInterceptedRef(it) | ||
| } | ||
| } | ||
| } |
Comment on lines
+62
to
+71
| @android.webkit.JavascriptInterface | ||
| public fun shouldInterceptCopy(): Boolean = | ||
| interceptEnabled && isSelecting | ||
|
|
||
| @android.webkit.JavascriptInterface | ||
| public fun onCopyIntercepted(text: String) { | ||
| coroutineScope.launch { | ||
| listener?.onCopyIntercepted(text) | ||
| } | ||
| } |
Comment on lines
+466
to
+473
| @android.webkit.JavascriptInterface | ||
| fun shouldInterceptCopy(): Boolean = | ||
| (listener?.shouldInterceptCopy == true) && isSelecting | ||
|
|
||
| @android.webkit.JavascriptInterface | ||
| fun onCopyIntercepted(text: String) { | ||
| listener?.onCopyIntercepted(text) | ||
| } |
mickael-menu
marked this pull request as draft
July 31, 2026 09:30
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #223, and #221.
Protected publications no longer have text selection disabled wholesale (the
user-select: nonehack). Instead, all navigators enforce the Content Protection's copy allowance themselves, mirroring the Swift toolkit's design:EpubNavigatorFragment.copySelection(): Try<Unit, CopyError>supports apps with a custom selection ActionMode callback.disableSelectionWhenProtectedis deprecated and ignored.CopyInterceptorscript;SelectionController.copySelection()and a newCopyListenerparameter report forbidden copies.disableCopyPaste()is removed.CopyErrortype and an internal atomic (Main.immediate + NonCancellable)copyToClipboardhelper: the allowance is consumed before writing the clipboard, and a cancelled scope can never strand a spent allowance. Denied copies leave the clipboard untouched on programmatic paths; interception paths clear it as a fail-safe for WebView builds ignoringpreventDefault().canCopy(text)/canPrint(pageCount)(with unit tests) and allowance-consumption failures are now always logged.copySelection(), snackbar/toast on denial); CHANGELOG, migration guide and LCP guide document the compliance warning: apps targeting the EDRLab certification MUST remove text-leaking system menu items (Share, Web Search, Translate) and never write the clipboard directly.preventDefault()suppressing the clipboard write across WebView versions, exact allowance decrements, and whether removingdisableCopyPaste()re-enables a keyboard copy path in PSPDFKit.🤖 Generated with Claude Code