From 725a66f07810628d36211a6472491eedec0ffadd Mon Sep 17 00:00:00 2001 From: Two-Ai <81279822+Two-Ai@users.noreply.github.com> Date: Sat, 18 Feb 2023 10:07:27 -0500 Subject: [PATCH] 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. (cherry picked from commit ffa8c8fd0746863e08b839f9d91102fe6d5d7887) # Conflicts: # app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt # app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt --- .../ui/reader/viewer/pager/PagerPageHolder.kt | 105 +++++++----------- .../viewer/webtoon/WebtoonBaseHolder.kt | 16 --- .../viewer/webtoon/WebtoonPageHolder.kt | 72 +++++------- .../ui/reader/viewer/webtoon/WebtoonViewer.kt | 7 -- 4 files changed, 66 insertions(+), 134 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt index c5ecf3b0a..592a8a08f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt @@ -22,11 +22,9 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlinx.coroutines.supervisorScope import logcat.LogPriority -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.withIOContext +import tachiyomi.core.util.lang.withUIContext import tachiyomi.core.util.system.logcat import tachiyomi.decoder.ImageDecoder import java.io.BufferedInputStream @@ -77,12 +75,6 @@ class PagerPageHolder( */ private var extraLoadJob: 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 { addView(progressIndicator) loadJob = scope.launch { loadPageAndProcessStatus(1) } @@ -99,7 +91,6 @@ class PagerPageHolder( loadJob = null extraLoadJob?.cancel() extraLoadJob = null - unsubscribeReadImageHeader() } /** @@ -136,14 +127,6 @@ class PagerPageHolder( } } - /** - * Unsubscribes from the read image header subscription. - */ - private fun unsubscribeReadImageHeader() { - readImageHeaderSubscription?.unsubscribe() - readImageHeaderSubscription = null - } - /** * Called when the page is queued. */ @@ -171,7 +154,7 @@ class PagerPageHolder( /** * Called when the page is ready. */ - private fun setImage() { + private suspend fun setImage() { if (extraPage == null) { progressIndicator.setProgress(0) } else { @@ -179,58 +162,52 @@ class PagerPageHolder( } errorLayout?.root?.isVisible = false - unsubscribeReadImageHeader() val streamFn = page.stream ?: return val streamFn2 = extraPage?.stream - readImageHeaderSubscription = Observable - .fromCallable { - val stream = streamFn().buffered(16) + + val (bais, isAnimated, background) = withIOContext { + streamFn().buffered(16).use { stream -> // SY --> - val stream2 = if (extraPage != null) streamFn2?.invoke()?.buffered(16) else null - val itemStream = if (viewer.config.dualPageSplit) { - process(item.first, stream) - } else { - mergePages(stream, stream2) - } - // SY <-- - val bais = ByteArrayInputStream(itemStream.readBytes()) - try { - val isAnimated = ImageUtil.isAnimatedAndSupported(bais) - bais.reset() - val background = if (!isAnimated && viewer.config.automaticBackground) { - ImageUtil.chooseBackground(context, bais) + (if (extraPage != null) streamFn2?.invoke()?.buffered(16) else null).use { stream2 -> + if (viewer.config.dualPageSplit) { + process(item.first, stream) } else { - null - } - bais.reset() - Triple(bais, isAnimated, background) - } finally { - stream.close() - itemStream.close() - } - } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .doOnNext { (bais, isAnimated, background) -> - bais.use { - setImage( - it, - isAnimated, - Config( - zoomDuration = viewer.config.doubleTapAnimDuration, - minimumScaleType = viewer.config.imageScaleType, - cropBorders = viewer.config.imageCropBorders, - zoomStartPosition = viewer.config.imageZoomType, - landscapeZoom = viewer.config.landscapeZoom, - ), - ) - if (!isAnimated) { - pageBackground = background + mergePages(stream, stream2) + }.use { itemStream -> + // SY <-- + val bais = ByteArrayInputStream(itemStream.readBytes()) + val isAnimated = ImageUtil.isAnimatedAndSupported(bais) + bais.reset() + val background = if (!isAnimated && viewer.config.automaticBackground) { + ImageUtil.chooseBackground(context, bais) + } else { + null + } + bais.reset() + Triple(bais, isAnimated, background) } } } - .subscribe({}, {}) + } + withUIContext { + bais.use { + setImage( + it, + isAnimated, + Config( + zoomDuration = viewer.config.doubleTapAnimDuration, + minimumScaleType = viewer.config.imageScaleType, + cropBorders = viewer.config.imageCropBorders, + zoomStartPosition = viewer.config.imageZoomType, + landscapeZoom = viewer.config.landscapeZoom, + ), + ) + if (!isAnimated) { + pageBackground = background + } + } + } } private fun process(page: ReaderPage, imageStream: BufferedInputStream): InputStream { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonBaseHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonBaseHolder.kt index 394f44d48..cbded3e7a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonBaseHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonBaseHolder.kt @@ -4,7 +4,6 @@ import android.content.Context import android.view.View import android.view.ViewGroup.LayoutParams import androidx.recyclerview.widget.RecyclerView -import rx.Subscription abstract class WebtoonBaseHolder( view: View, @@ -21,21 +20,6 @@ abstract class WebtoonBaseHolder( */ 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. */ diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt index eed0bb6d0..1ca082154 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt @@ -25,11 +25,10 @@ import kotlinx.coroutines.MainScope import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlinx.coroutines.supervisorScope -import rx.Observable -import rx.Subscription -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers +import kotlinx.coroutines.suspendCancellableCoroutine 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.InputStream @@ -79,12 +78,6 @@ class WebtoonPageHolder( */ 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 { refreshLayoutParams() @@ -121,7 +114,6 @@ class WebtoonPageHolder( override fun recycle() { loadJob?.cancel() loadJob = null - unsubscribeReadImageHeader() removeErrorLayout() 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. */ @@ -197,40 +181,34 @@ class WebtoonPageHolder( /** * Called when the page is ready. */ - private fun setImage() { + private suspend fun setImage() { progressIndicator.setProgress(0) removeErrorLayout() - unsubscribeReadImageHeader() val streamFn = page?.stream ?: return - var openStream: InputStream? = null - readImageHeaderSubscription = Observable - .fromCallable { - val stream = streamFn().buffered(16) - openStream = process(stream) + val (openStream, isAnimated) = withIOContext { + val stream = streamFn().buffered(16) + val openStream = process(stream) - ImageUtil.isAnimatedAndSupported(stream) - } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .doOnNext { isAnimated -> - frame.setImage( - openStream!!, - isAnimated, - ReaderPageImageView.Config( - zoomDuration = viewer.config.doubleTapAnimDuration, - minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_FIT_WIDTH, - cropBorders = (viewer.config.imageCropBorders && viewer.isContinuous) || (viewer.config.continuousCropBorders && !viewer.isContinuous), - ), - ) - } - // Keep the Rx stream alive to close the input stream only when unsubscribed - .flatMap { Observable.never() } - .doOnUnsubscribe { openStream?.close() } - .subscribe({}, {}) - - addSubscription(readImageHeaderSubscription) + val isAnimated = ImageUtil.isAnimatedAndSupported(stream) + Pair(openStream, isAnimated) + } + withUIContext { + frame.setImage( + openStream, + isAnimated, + ReaderPageImageView.Config( + zoomDuration = viewer.config.doubleTapAnimDuration, + minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_FIT_WIDTH, + cropBorders = (viewer.config.imageCropBorders && viewer.isContinuous) || (viewer.config.continuousCropBorders && !viewer.isContinuous), + ), + ) + } + // Suspend the coroutine to close the input stream only when the WebtoonPageHolder is recycled + suspendCancellableCoroutine { continuation -> + continuation.invokeOnCancellation { openStream.close() } + } } private fun process(imageStream: BufferedInputStream): InputStream { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt index 53e0a0271..a688315b5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt @@ -22,7 +22,6 @@ import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation.NavigationRegion import kotlinx.coroutines.MainScope import kotlinx.coroutines.cancel -import rx.subscriptions.CompositeSubscription import tachiyomi.core.util.system.logcat import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -75,11 +74,6 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr /* [EXH] private */ var currentPage: Any? = null - /** - * Subscriptions to keep while this viewer is used. - */ - val subscriptions = CompositeSubscription() - private val threshold: Int = Injekt.get() .readerHideThreshold() @@ -203,7 +197,6 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr override fun destroy() { super.destroy() scope.cancel() - subscriptions.unsubscribe() } /**