Remove RxJava in PageHolder (#9103)

Inline readImageHeaderSubscription in PageHolder

Inline readImageHeaderSubscription in PagerPageHolder and
WebtoonPageHolder by converting setImage() into a suspend function.
The image processing runs in the loadPageAndProcessStatus
continuation.

Use suspendCancellableCoroutine as a substitute for doOnUnsubscribe
in WebtoonPageHolder.
Closing openStream after the frame.setImage but before the PageHolder
is recycled causes the page display to fail for reasons that are not
currently understood.

Remove subscription handling from WebtoonViewer/WebtoonBaseHolder as
it is no longer used.
This commit is contained in:
Two-Ai 2023-02-18 10:07:27 -05:00 committed by GitHub
parent 0ef7650c1a
commit ffa8c8fd07
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 49 additions and 119 deletions

View File

@ -20,11 +20,9 @@ import kotlinx.coroutines.MainScope
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.supervisorScope
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import tachiyomi.core.util.lang.launchIO import tachiyomi.core.util.lang.launchIO
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.lang.withUIContext
import java.io.BufferedInputStream import java.io.BufferedInputStream
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.InputStream import java.io.InputStream
@ -66,12 +64,6 @@ class PagerPageHolder(
*/ */
private var loadJob: Job? = null private var loadJob: Job? = null
/**
* Subscription used to read the header of the image. This is needed in order to instantiate
* the appropiate image view depending if the image is animated (GIF).
*/
private var readImageHeaderSubscription: Subscription? = null
init { init {
addView(progressIndicator) addView(progressIndicator)
loadJob = scope.launch { loadPageAndProcessStatus() } loadJob = scope.launch { loadPageAndProcessStatus() }
@ -85,7 +77,6 @@ class PagerPageHolder(
super.onDetachedFromWindow() super.onDetachedFromWindow()
loadJob?.cancel() loadJob?.cancel()
loadJob = null loadJob = null
unsubscribeReadImageHeader()
} }
/** /**
@ -119,14 +110,6 @@ class PagerPageHolder(
} }
} }
/**
* Unsubscribes from the read image header subscription.
*/
private fun unsubscribeReadImageHeader() {
readImageHeaderSubscription?.unsubscribe()
readImageHeaderSubscription = null
}
/** /**
* Called when the page is queued. * Called when the page is queued.
*/ */
@ -154,19 +137,16 @@ class PagerPageHolder(
/** /**
* Called when the page is ready. * Called when the page is ready.
*/ */
private fun setImage() { private suspend fun setImage() {
progressIndicator.setProgress(0) progressIndicator.setProgress(0)
errorLayout?.root?.isVisible = false errorLayout?.root?.isVisible = false
unsubscribeReadImageHeader()
val streamFn = page.stream ?: return val streamFn = page.stream ?: return
readImageHeaderSubscription = Observable val (bais, isAnimated, background) = withIOContext {
.fromCallable { streamFn().buffered(16).use { stream ->
val stream = streamFn().buffered(16) process(item, stream).use { itemStream ->
val itemStream = process(item, stream) val bais = ByteArrayInputStream(itemStream.readBytes())
val bais = ByteArrayInputStream(itemStream.readBytes())
try {
val isAnimated = ImageUtil.isAnimatedAndSupported(bais) val isAnimated = ImageUtil.isAnimatedAndSupported(bais)
bais.reset() bais.reset()
val background = if (!isAnimated && viewer.config.automaticBackground) { val background = if (!isAnimated && viewer.config.automaticBackground) {
@ -176,32 +156,27 @@ class PagerPageHolder(
} }
bais.reset() bais.reset()
Triple(bais, isAnimated, background) Triple(bais, isAnimated, background)
} finally {
stream.close()
itemStream.close()
} }
} }
.subscribeOn(Schedulers.io()) }
.observeOn(AndroidSchedulers.mainThread()) withUIContext {
.doOnNext { (bais, isAnimated, background) -> bais.use {
bais.use { setImage(
setImage( it,
it, isAnimated,
isAnimated, Config(
Config( zoomDuration = viewer.config.doubleTapAnimDuration,
zoomDuration = viewer.config.doubleTapAnimDuration, minimumScaleType = viewer.config.imageScaleType,
minimumScaleType = viewer.config.imageScaleType, cropBorders = viewer.config.imageCropBorders,
cropBorders = viewer.config.imageCropBorders, zoomStartPosition = viewer.config.imageZoomType,
zoomStartPosition = viewer.config.imageZoomType, landscapeZoom = viewer.config.landscapeZoom,
landscapeZoom = viewer.config.landscapeZoom, ),
), )
) if (!isAnimated) {
if (!isAnimated) { pageBackground = background
pageBackground = background
}
} }
} }
.subscribe({}, {}) }
} }
private fun process(page: ReaderPage, imageStream: BufferedInputStream): InputStream { private fun process(page: ReaderPage, imageStream: BufferedInputStream): InputStream {

View File

@ -4,7 +4,6 @@ import android.content.Context
import android.view.View import android.view.View
import android.view.ViewGroup.LayoutParams import android.view.ViewGroup.LayoutParams
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import rx.Subscription
abstract class WebtoonBaseHolder( abstract class WebtoonBaseHolder(
view: View, view: View,
@ -21,21 +20,6 @@ abstract class WebtoonBaseHolder(
*/ */
open fun recycle() {} open fun recycle() {}
/**
* Adds a subscription to a list of subscriptions that will automatically unsubscribe when the
* activity or the reader is destroyed.
*/
protected fun addSubscription(subscription: Subscription?) {
viewer.subscriptions.add(subscription)
}
/**
* Removes a subscription from the list of subscriptions.
*/
protected fun removeSubscription(subscription: Subscription?) {
subscription?.let { viewer.subscriptions.remove(it) }
}
/** /**
* Extension method to set layout params to wrap content on this view. * Extension method to set layout params to wrap content on this view.
*/ */

View File

@ -25,11 +25,10 @@ import kotlinx.coroutines.MainScope
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.supervisorScope
import rx.Observable import kotlinx.coroutines.suspendCancellableCoroutine
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import tachiyomi.core.util.lang.launchIO import tachiyomi.core.util.lang.launchIO
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.lang.withUIContext
import java.io.BufferedInputStream import java.io.BufferedInputStream
import java.io.InputStream import java.io.InputStream
@ -79,12 +78,6 @@ class WebtoonPageHolder(
*/ */
private var loadJob: Job? = null private var loadJob: Job? = null
/**
* Subscription used to read the header of the image. This is needed in order to instantiate
* the appropriate image view depending if the image is animated (GIF).
*/
private var readImageHeaderSubscription: Subscription? = null
init { init {
refreshLayoutParams() refreshLayoutParams()
@ -121,7 +114,6 @@ class WebtoonPageHolder(
override fun recycle() { override fun recycle() {
loadJob?.cancel() loadJob?.cancel()
loadJob = null loadJob = null
unsubscribeReadImageHeader()
removeErrorLayout() removeErrorLayout()
frame.recycle() frame.recycle()
@ -159,14 +151,6 @@ class WebtoonPageHolder(
} }
} }
/**
* Unsubscribes from the read image header subscription.
*/
private fun unsubscribeReadImageHeader() {
removeSubscription(readImageHeaderSubscription)
readImageHeaderSubscription = null
}
/** /**
* Called when the page is queued. * Called when the page is queued.
*/ */
@ -197,40 +181,34 @@ class WebtoonPageHolder(
/** /**
* Called when the page is ready. * Called when the page is ready.
*/ */
private fun setImage() { private suspend fun setImage() {
progressIndicator.setProgress(0) progressIndicator.setProgress(0)
removeErrorLayout() removeErrorLayout()
unsubscribeReadImageHeader()
val streamFn = page?.stream ?: return val streamFn = page?.stream ?: return
var openStream: InputStream? = null val (openStream, isAnimated) = withIOContext {
readImageHeaderSubscription = Observable val stream = streamFn().buffered(16)
.fromCallable { val openStream = process(stream)
val stream = streamFn().buffered(16)
openStream = process(stream)
ImageUtil.isAnimatedAndSupported(stream) val isAnimated = ImageUtil.isAnimatedAndSupported(stream)
} Pair(openStream, isAnimated)
.subscribeOn(Schedulers.io()) }
.observeOn(AndroidSchedulers.mainThread()) withUIContext {
.doOnNext { isAnimated -> frame.setImage(
frame.setImage( openStream,
openStream!!, isAnimated,
isAnimated, ReaderPageImageView.Config(
ReaderPageImageView.Config( zoomDuration = viewer.config.doubleTapAnimDuration,
zoomDuration = viewer.config.doubleTapAnimDuration, minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_FIT_WIDTH,
minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_FIT_WIDTH, cropBorders = viewer.config.imageCropBorders,
cropBorders = viewer.config.imageCropBorders, ),
), )
) }
} // Suspend the coroutine to close the input stream only when the WebtoonPageHolder is recycled
// Keep the Rx stream alive to close the input stream only when unsubscribed suspendCancellableCoroutine<Nothing> { continuation ->
.flatMap { Observable.never<Unit>() } continuation.invokeOnCancellation { openStream.close() }
.doOnUnsubscribe { openStream?.close() } }
.subscribe({}, {})
addSubscription(readImageHeaderSubscription)
} }
private fun process(imageStream: BufferedInputStream): InputStream { private fun process(imageStream: BufferedInputStream): InputStream {

View File

@ -22,7 +22,6 @@ import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation.NavigationRegion import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation.NavigationRegion
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import rx.subscriptions.CompositeSubscription
import tachiyomi.core.util.system.logcat import tachiyomi.core.util.system.logcat
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -74,11 +73,6 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
*/ */
private var currentPage: Any? = null private var currentPage: Any? = null
/**
* Subscriptions to keep while this viewer is used.
*/
val subscriptions = CompositeSubscription()
private val threshold: Int = private val threshold: Int =
Injekt.get<ReaderPreferences>() Injekt.get<ReaderPreferences>()
.readerHideThreshold() .readerHideThreshold()
@ -196,7 +190,6 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
override fun destroy() { override fun destroy() {
super.destroy() super.destroy()
scope.cancel() scope.cancel()
subscriptions.unsubscribe()
} }
/** /**