Skip to content

Fix crash when restoring the fragment hierarchy after process death #418

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Nov 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,24 @@ All notable changes to this project will be documented in this file. Take a look
* Support for non-linear EPUB resources with an opt-in in reading apps (contributed by @chrfalch in [#375](https://github.com/readium/kotlin-toolkit/pull/375) and [#376](https://github.com/readium/kotlin-toolkit/pull/376)).
1. Override loading non-linear resources with `VisualNavigator.Listener.shouldJumpToLink()`.
2. Present a new `EpubNavigatorFragment` by providing a custom `readingOrder` with only this resource to the constructor.
* Added dummy navigator fragment factories to prevent crashes caused by Android restoring the fragments after a process death.
* To use it, set the dummy fragment factory when you don't have access to the `Publication` instance. Then, either finish the `Activity` or pop the fragment from the UI before it resumes.
```kotlin
override fun onCreate(savedInstanceState: Bundle?) {
val publication = model.publication ?: run {
childFragmentManager.fragmentFactory = EpubNavigatorFragment.createDummyFactory()
super.onCreate(savedInstanceState)

requireActivity().finish()
// or
navController?.popBackStack()

return
}

// Create the real navigator factory as usual...
}
```

#### Streamer

Expand All @@ -39,7 +57,7 @@ All notable changes to this project will be documented in this file. Take a look

#### Streamer

* Fix issue with the TTS starting from the beginning of the chapter instead of the current position.
* Fixed issue with the TTS starting from the beginning of the chapter instead of the current position.

## [2.3.0]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ import org.readium.r2.navigator.preferences.ReadingProgression
import org.readium.r2.shared.ExperimentalReadiumApi
import org.readium.r2.shared.fetcher.Resource
import org.readium.r2.shared.publication.Link
import org.readium.r2.shared.publication.LocalizedString
import org.readium.r2.shared.publication.Manifest
import org.readium.r2.shared.publication.Metadata
import org.readium.r2.shared.publication.Publication
import timber.log.Timber

Expand All @@ -36,6 +39,28 @@ class PdfiumDocumentFragment internal constructor(
private val navigatorListener: PdfDocumentFragment.Listener?
) : PdfDocumentFragment<PdfiumSettings>() {

// Dummy constructor to address https://github.com/readium/kotlin-toolkit/issues/395
constructor() : this(
publication = Publication(
manifest = Manifest(
metadata = Metadata(
identifier = "readium:dummy",
localizedTitle = LocalizedString("")
)
)
),
link = Link(href = "publication.pdf", type = "application/pdf"),
initialPageIndex = 0,
settings = PdfiumSettings(
fit = Fit.WIDTH,
pageSpacing = 0.0,
readingProgression = ReadingProgression.LTR,
scrollAxis = Axis.VERTICAL
),
appListener = null,
navigatorListener = null
)

interface Listener {
/** Called when configuring [PDFView]. */
fun onConfigurePdfView(configurator: PDFView.Configurator) {}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.readium.r2.navigator

import org.readium.r2.shared.publication.LocalizedString
import org.readium.r2.shared.publication.Manifest
import org.readium.r2.shared.publication.Metadata
import org.readium.r2.shared.publication.Publication

object RestorationNotSupportedException : Exception(
"Restoration of the navigator fragment after process death is not supported. You must pop it from the back stack or finish the host Activity before `onResume`."
)

internal val dummyPublication = Publication(
Manifest(
metadata = Metadata(
identifier = "readium:dummy",
localizedTitle = LocalizedString("")
)
)
)
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,11 @@ class EpubNavigatorFragment internal constructor(

override fun onResume() {
super.onResume()

if (publication == dummyPublication) {
throw RestorationNotSupportedException
}

notifyCurrentLocation()
}

Expand Down Expand Up @@ -1066,6 +1071,27 @@ class EpubNavigatorFragment internal constructor(
)
}

/**
* Creates a factory for a dummy [EpubNavigatorFragment].
*
* Used when Android restore the [EpubNavigatorFragment] after the process was killed. You
* need to make sure the fragment is removed from the screen before [onResume] is called.
*/
fun createDummyFactory(): FragmentFactory = createFragmentFactory {
EpubNavigatorFragment(
publication = dummyPublication,
baseUrl = null,
initialLocator = Locator(href = "#", type = "application/xhtml+xml"),
readingOrder = null,
initialPreferences = EpubPreferences(),
listener = null,
paginationListener = null,
epubLayout = EpubLayout.REFLOWABLE,
defaults = EpubDefaults(),
configuration = Configuration()
)
}

/**
* Returns a URL to the application asset at [path], served in the web views.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.runBlocking
import org.readium.r2.navigator.RestorationNotSupportedException
import org.readium.r2.navigator.SimplePresentation
import org.readium.r2.navigator.VisualNavigator
import org.readium.r2.navigator.databinding.ActivityR2ViewpagerBinding
import org.readium.r2.navigator.dummyPublication
import org.readium.r2.navigator.extensions.layoutDirectionIsRTL
import org.readium.r2.navigator.pager.R2CbzPageFragment
import org.readium.r2.navigator.pager.R2PagerAdapter
Expand Down Expand Up @@ -155,6 +157,14 @@ class ImageNavigatorFragment private constructor(
notifyCurrentLocation()
}

override fun onResume() {
super.onResume()

if (publication == dummyPublication) {
throw RestorationNotSupportedException
}
}

override fun onDestroyView() {
super.onDestroyView()
_binding = null
Expand All @@ -173,10 +183,10 @@ class ImageNavigatorFragment private constructor(
}

private fun notifyCurrentLocation() {
val locator = positions[resourcePager.currentItem]
if (locator == _currentLocator.value) {
return
}
val locator = positions.getOrNull(resourcePager.currentItem)
?.takeUnless { it == _currentLocator.value }
?: return

_currentLocator.value = locator
}

Expand Down Expand Up @@ -240,5 +250,19 @@ class ImageNavigatorFragment private constructor(
listener: Listener? = null
): FragmentFactory =
createFragmentFactory { ImageNavigatorFragment(publication, initialLocator, listener) }

/**
* Creates a factory for a dummy [ImageNavigatorFragment].
*
* Used when Android restore the [ImageNavigatorFragment] after the process was killed. You
* need to make sure the fragment is removed from the screen before `onResume` is called.
*/
fun createDummyFactory(): FragmentFactory = createFragmentFactory {
ImageNavigatorFragment(
publication = dummyPublication,
initialLocator = Locator(href = "#", type = "image/jpg"),
listener = null
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import org.readium.r2.navigator.R
import org.readium.r2.navigator.RestorationNotSupportedException
import org.readium.r2.navigator.VisualNavigator
import org.readium.r2.navigator.dummyPublication
import org.readium.r2.navigator.extensions.page
import org.readium.r2.navigator.preferences.Configurable
import org.readium.r2.navigator.preferences.PreferencesEditor
Expand Down Expand Up @@ -87,15 +89,35 @@ class PdfNavigatorFragment<S : Configurable.Settings, P : Configurable.Preferenc
listener, pdfEngineProvider
)
}

/**
* Creates a factory for a dummy [PdfNavigatorFragment].
*
* Used when Android restore the [PdfNavigatorFragment] after the process was killed. You need
* to make sure the fragment is removed from the screen before `onResume` is called.
*/
fun <P : Configurable.Preferences<P>> createDummyFactory(
pdfEngineProvider: PdfEngineProvider<*, P, *>
): FragmentFactory = createFragmentFactory {
PdfNavigatorFragment(
publication = dummyPublication,
initialLocator = Locator(href = "#", type = "application/pdf"),
initialPreferences = pdfEngineProvider.createEmptyPreferences(),
listener = null,
pdfEngineProvider = pdfEngineProvider
)
}
}

init {
require(!publication.isRestricted) { "The provided publication is restricted. Check that any DRM was properly unlocked using a Content Protection." }

require(
publication.readingOrder.count() == 1 &&
publication.readingOrder.first().mediaType.matches(MediaType.PDF)
) { "[PdfNavigatorFragment] currently supports only publications with a single PDF for reading order" }
if (publication != dummyPublication) {
require(
publication.readingOrder.count() == 1 &&
publication.readingOrder.first().mediaType.matches(MediaType.PDF)
) { "[PdfNavigatorFragment] currently supports only publications with a single PDF for reading order" }
}
}

// Configurable
Expand Down Expand Up @@ -167,6 +189,14 @@ class PdfNavigatorFragment<S : Configurable.Settings, P : Configurable.Preferenc
}
}

override fun onResume() {
super.onResume()

if (publication == dummyPublication) {
throw RestorationNotSupportedException
}
}

private suspend fun createPdfDocumentFragment(locator: Locator, settings: S): PdfDocumentFragment<S>? {
val link = publication.linkWithHref(locator.href) ?: return null

Expand Down
Loading