diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/HardwareImageFailure.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/HardwareImageFailure.kt new file mode 100644 index 00000000000..bd5c19873fc --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/HardwareImageFailure.kt @@ -0,0 +1,18 @@ +package com.lagradost.cloudstream3.utils + +internal object HardwareImageFailure { + fun isRecoverable(error: Throwable): Boolean { + val visited = HashSet() + var current: Throwable? = error + var recoverable = false + while (current != null && visited.add(current)) { + if (current is Error || current is java.util.concurrent.CancellationException) return false + val message = current.message.orEmpty().lowercase() + if (("hardware" in message && "bitmap" in message) || + "ahardwarebuffer" in message || + ("graphicbuffer" in message && "allocat" in message)) recoverable = true + current = current.cause + } + return recoverable + } +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ImageCompatibilityInterceptor.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ImageCompatibilityInterceptor.kt new file mode 100644 index 00000000000..9b02475131e --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ImageCompatibilityInterceptor.kt @@ -0,0 +1,29 @@ +package com.lagradost.cloudstream3.utils + +import android.graphics.Bitmap +import coil3.intercept.Interceptor +import coil3.request.ErrorResult +import coil3.request.ImageResult +import coil3.request.allowHardware +import coil3.request.bitmapConfig +import java.util.concurrent.atomic.AtomicBoolean + +internal class ImageCompatibilityInterceptor : Interceptor { + private val softwareRequired = AtomicBoolean(false) + + override suspend fun intercept(chain: Interceptor.Chain): ImageResult { + val request = chain.request + if (request.allowHardware && softwareRequired.get()) { + return chain.withRequest(request.newBuilder().allowHardware(false) + .bitmapConfig(Bitmap.Config.ARGB_8888).build()).proceed() + } + val result = chain.proceed() + if (request.allowHardware && result is ErrorResult && + HardwareImageFailure.isRecoverable(result.throwable)) { + softwareRequired.set(true) + return chain.withRequest(request.newBuilder().allowHardware(false) + .bitmapConfig(Bitmap.Config.ARGB_8888).build()).proceed() + } + return result + } +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ImageModuleCoil.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ImageModuleCoil.kt index 96193fe45f7..32a0551c7ae 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ImageModuleCoil.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ImageModuleCoil.kt @@ -58,6 +58,7 @@ object ImageLoader { /** Pass interceptors with care, unnecessary passing tokens to servers or image hosting services causes unauthorized exceptions **/ .components { + add(ImageCompatibilityInterceptor()) add(OkHttpNetworkFetcherFactory(callFactory = { buildDefaultClient(context) })) if (isBrokenHardware) { add(BitmapFactoryDecoder.Factory()) diff --git a/app/src/test/java/com/lagradost/cloudstream3/utils/HardwareImageFailureTest.kt b/app/src/test/java/com/lagradost/cloudstream3/utils/HardwareImageFailureTest.kt new file mode 100644 index 00000000000..92ca16cd6ee --- /dev/null +++ b/app/src/test/java/com/lagradost/cloudstream3/utils/HardwareImageFailureTest.kt @@ -0,0 +1,55 @@ +package com.lagradost.cloudstream3.utils + +import java.io.IOException +import java.util.concurrent.CancellationException +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class HardwareImageFailureTest { + @Test fun recoversAllocationFailureWithoutModelDetection() { + assertTrue(HardwareImageFailure.isRecoverable(IllegalStateException("failed to allocate hardware Bitmap!"))) + } + + @Test fun recoversWrappedSoftwareCanvasFailure() { + assertTrue(HardwareImageFailure.isRecoverable(IOException("decode failed", + IllegalArgumentException("Software rendering does not support hardware bitmaps")))) + } + + @Test fun doesNotRetryHttpFailuresAsDecodeFailures() { + assertFalse(HardwareImageFailure.isRecoverable(IOException("HTTP 403"))) + } + + @Test fun doesNotRetryOutOfMemoryEvenWhenWrappedByHardwareError() { + assertFalse(HardwareImageFailure.isRecoverable(IllegalStateException("hardware bitmap", + OutOfMemoryError("heap exhausted")))) + } + + @Test fun doesNotRetryCancellation() { + assertFalse(HardwareImageFailure.isRecoverable(CancellationException("hardware bitmap request cancelled"))) + } + + @Test fun recoversGraphicBufferAllocationFailure() { + assertTrue(HardwareImageFailure.isRecoverable(IllegalStateException("GraphicBuffer allocation failed"))) + } + + @Test fun recoversAHardwareBufferFailure() { + assertTrue(HardwareImageFailure.isRecoverable(IllegalStateException("AHardwareBuffer allocation failed"))) + } + + @Test fun rejectsUnrelatedDecodeFailure() { + assertFalse(HardwareImageFailure.isRecoverable(IllegalArgumentException("Invalid image data"))) + } + + @Test fun doesNotRetryFatalErrors() { + assertFalse(HardwareImageFailure.isRecoverable(StackOverflowError("hardware bitmap"))) + } + + @Test fun terminatesForCyclicCauseChains() { + val first = IllegalStateException("hardware bitmap allocation failed") + val second = IllegalArgumentException("decode failed") + first.initCause(second) + second.initCause(first) + assertTrue(HardwareImageFailure.isRecoverable(first)) + } +}