From 29195ff5eca4689457a44b3b69ef8f1e86c7a7e6 Mon Sep 17 00:00:00 2001 From: wildan-m Date: Wed, 17 Jun 2026 23:54:02 +0700 Subject: [PATCH 1/4] Honor AlreadyClosedBehavior in openPage/openTextPage These were the only PdfDocument methods using an unconditional check(!isClosed) instead of handleAlreadyClosed(), so a configured AlreadyClosedBehavior.IGNORE never reached them. A render that lands on a just-closed document threw IllegalStateException on the renderer thread (PdfiumCore.renderPageBitmap -> openPage), which the barteksc RenderingHandler does not catch, crashing the app. Now they route through handleAlreadyClosed() like every sibling method and, when IGNORE is set, return a page bound to the closed document so all further ops no-op via the existing doc.isClosed guards. --- .../io/legere/pdfiumandroid/PdfDocument.kt | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/pdfiumandroid/src/main/java/io/legere/pdfiumandroid/PdfDocument.kt b/pdfiumandroid/src/main/java/io/legere/pdfiumandroid/PdfDocument.kt index 4107903..0bd5194 100644 --- a/pdfiumandroid/src/main/java/io/legere/pdfiumandroid/PdfDocument.kt +++ b/pdfiumandroid/src/main/java/io/legere/pdfiumandroid/PdfDocument.kt @@ -141,7 +141,18 @@ class PdfDocument( */ fun openPage(pageIndex: Int): PdfPage { synchronized(PdfiumCore.lock) { - check(!isClosed) { "Already closed" } + // Honor the configured AlreadyClosedBehavior instead of always throwing. + // Every other PdfDocument method routes its closed-state check through + // handleAlreadyClosed(); openPage/openTextPage were the only ones that did + // not, so AlreadyClosedBehavior.IGNORE never reached the exact call that + // crashes when a render lands on a just-closed document (see openPage in + // PdfiumCore.renderPageBitmap). When IGNORE is configured we return a page + // bound to this (closed) document; every PdfPage operation already guards on + // doc.isClosed via handleAlreadyClosed(), so the render is silently dropped + // with no native call instead of throwing on the renderer thread. + if (handleAlreadyClosed(isClosed)) { + return PdfPage(this, pageIndex, INVALID_PAGE_PTR, pageMap) + } if (pageMap.containsKey(pageIndex)) { pageMap[pageIndex]?.let { it.count++ @@ -376,7 +387,11 @@ class PdfDocument( @Deprecated("Use PdfPage.openTextPage instead", ReplaceWith("page.openTextPage()")) fun openTextPage(page: PdfPage): PdfTextPage { synchronized(PdfiumCore.lock) { - check(!isClosed) { "Already closed" } + // See openPage(): honor AlreadyClosedBehavior.IGNORE instead of throwing so a + // text-page open that races with document teardown is dropped, not fatal. + if (handleAlreadyClosed(isClosed)) { + return PdfTextPage(this, page.pageIndex, INVALID_PAGE_PTR, textPageMap) + } if (textPageMap.containsKey(page.pageIndex)) { textPageMap[page.pageIndex]?.let { it.count++ @@ -483,6 +498,12 @@ class PdfDocument( companion object { private val TAG = PdfDocument::class.java.name + // Sentinel native pointer used for the no-op PdfPage/PdfTextPage returned when a + // page is opened on an already-closed document under AlreadyClosedBehavior.IGNORE. + // It is never dereferenced: every PdfPage/PdfTextPage operation short-circuits via + // handleAlreadyClosed(doc.isClosed) before touching the native pointer. + private const val INVALID_PAGE_PTR = -1L + const val FPDF_INCREMENTAL = 1 const val FPDF_NO_INCREMENTAL = 2 const val FPDF_REMOVE_SECURITY = 3 From fcfe6948b335e74df9a1806336ef2e46550a1906 Mon Sep 17 00:00:00 2001 From: wildan-m Date: Thu, 18 Jun 2026 00:00:58 +0700 Subject: [PATCH 2/4] Add jitpack.yml: build only :pdfiumandroid on JDK 17 --- jitpack.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 jitpack.yml diff --git a/jitpack.yml b/jitpack.yml new file mode 100644 index 0000000..f46b941 --- /dev/null +++ b/jitpack.yml @@ -0,0 +1,4 @@ +jdk: + - openjdk17 +install: + - ./gradlew :pdfiumandroid:publishToMavenLocal -x test From e6c06c2905d1adf8b76f28c7440a2006033ca4c8 Mon Sep 17 00:00:00 2001 From: wildan-m Date: Thu, 18 Jun 2026 00:05:03 +0700 Subject: [PATCH 3/4] jitpack: accept Android SDK/NDK licenses before build --- jitpack.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/jitpack.yml b/jitpack.yml index f46b941..1cfbcfe 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -1,4 +1,7 @@ jdk: - openjdk17 +before_install: + - yes | "${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager" --licenses > /dev/null 2>&1 || true + - yes | sdkmanager --licenses > /dev/null 2>&1 || true install: - - ./gradlew :pdfiumandroid:publishToMavenLocal -x test + - ./gradlew :pdfiumandroid:publishToMavenLocal -x test --no-daemon From 6e60e76425b537f910cd0edfbcb75373b75585c9 Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Sun, 12 Jul 2026 03:18:15 +0700 Subject: [PATCH 4/4] Add instrumentation regression test for the openPage() Already-closed fix Covers the exact APP-1VG frame from Expensify/App#93839: with AlreadyClosedBehavior.IGNORE set, opening a page on an already-closed PdfDocument must return instead of throwing. Fails on stock 1.0.35 (IllegalStateException: Already closed at PdfDocument.openPage); passes with the openPage()/openTextPage() fix on this branch. --- .../AlreadyClosedOpenPageTest.kt | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 pdfiumandroid/src/androidTest/java/io/legere/pdfiumandroid/AlreadyClosedOpenPageTest.kt diff --git a/pdfiumandroid/src/androidTest/java/io/legere/pdfiumandroid/AlreadyClosedOpenPageTest.kt b/pdfiumandroid/src/androidTest/java/io/legere/pdfiumandroid/AlreadyClosedOpenPageTest.kt new file mode 100644 index 0000000..f97d21f --- /dev/null +++ b/pdfiumandroid/src/androidTest/java/io/legere/pdfiumandroid/AlreadyClosedOpenPageTest.kt @@ -0,0 +1,54 @@ +package io.legere.pdfiumandroid + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import io.legere.pdfiumandroid.base.BasePDFTest +import io.legere.pdfiumandroid.util.AlreadyClosedBehavior +import io.legere.pdfiumandroid.util.Config +import io.legere.pdfiumandroid.util.pdfiumConfig +import org.junit.After +import org.junit.Test +import org.junit.runner.RunWith + +/** + * E/App #93839 (Sentry APP-1VG) — Android "Already closed" crash on receipt PDF render. + * + * The production crash is a non-deterministic renderer-thread-vs-teardown race: a render + * lands on PdfDocument.openPage(0) after the document was already close()d. The app sets + * AlreadyClosedBehavior.IGNORE (react-native-pdf patch 002) so such a late call should be + * swallowed — but stock io.legere:pdfiumandroid 1.0.35 has openPage()/openTextPage() throw + * unconditionally, ignoring IGNORE, so the IllegalStateException crashes the renderer thread. + * + * This forces that exact frame deterministically: IGNORE -> newDocument -> close() -> openPage(0) + * + * Baseline (openPage does NOT honor IGNORE): FAILS — IllegalStateException("Already closed") + * at PdfDocument.openPage(PdfDocument.kt) == APP-1VG + * Fork (openPage honors IGNORE): PASSES — openPage returns instead of throwing + */ +@RunWith(AndroidJUnit4::class) +class AlreadyClosedOpenPageTest : BasePDFTest() { + + @After + fun tearDown() { + // Don't leak the IGNORE config into other test classes. + pdfiumConfig = Config() + } + + @Test + fun openPageAfterClose_withIgnore_doesNotCrash() { + val pdfBytes = getPdfBytes("f01.pdf") + assertThat(pdfBytes).isNotNull() + + // Configure exactly as the app does (react-native-pdf patch 002): IGNORE via the + // PdfiumCore constructor. (PdfiumCore.init overwrites the global pdfiumConfig, so + // setting the global directly would be wiped by construction.) + val core = PdfiumCore(config = Config(alreadyClosedBehavior = AlreadyClosedBehavior.IGNORE)) + val doc = core.newDocument(pdfBytes) + doc.close() + + // The exact crashing call from the APP-1VG stack. With IGNORE set this must NOT throw. + // On stock 1.0.35 it throws IllegalStateException("Already closed") here. + val page = doc.openPage(0) + assertThat(page).isNotNull() + } +}