diff --git a/jitpack.yml b/jitpack.yml new file mode 100644 index 0000000..1cfbcfe --- /dev/null +++ b/jitpack.yml @@ -0,0 +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 --no-daemon 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() + } +} 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