diff --git a/app/src/main/kotlin/io/github/landwarderer/futon/core/network/CloudFlareInterceptor.kt b/app/src/main/kotlin/io/github/landwarderer/futon/core/network/CloudFlareInterceptor.kt index 64dfcb05c..79a01d117 100644 --- a/app/src/main/kotlin/io/github/landwarderer/futon/core/network/CloudFlareInterceptor.kt +++ b/app/src/main/kotlin/io/github/landwarderer/futon/core/network/CloudFlareInterceptor.kt @@ -1,44 +1,91 @@ package io.github.landwarderer.futon.core.network +import android.os.Build +import io.github.landwarderer.futon.core.exceptions.CloudFlareBlockedException +import io.github.landwarderer.futon.core.exceptions.CloudFlareProtectedException +import io.github.landwarderer.futon.core.util.ext.printStackTraceDebug import okhttp3.Interceptor import okhttp3.Response import okio.IOException -import io.github.landwarderer.futon.core.exceptions.CloudFlareBlockedException -import io.github.landwarderer.futon.core.exceptions.CloudFlareProtectedException import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.network.CloudFlareHelper class CloudFlareInterceptor : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + val response = chain.proceed(request) + + // Android 6 (SDK 23) has a bug in the implementation of the + // "International Components for Unicode (ICU)" charset decoder used by "java.nio". + // The error occurs when reading a stream into a String if the byte sequence + // is interpreted in a way that causes the ICU decoder to flush its buffer incorrectly, + // resulting in a negative position in the underlying "ByteBuffer". + // + // As a workaround, we need to manually decode the bytes to avoid the ICU "Bad position" bug + // when running on Android 6. + val protectionType = if (Build.VERSION.SDK_INT == 23) { + try { + // Peek up to 512 bytes safely. + val bodyBytes = response.peekBody(512).bytes() + val bodyString = String(bodyBytes, Charsets.UTF_8) + + when { + // Check for common Cloudflare challenge indicators + bodyString.contains("cf-challenge") || + bodyString.contains("ray_id") || + bodyString.contains("jschl_vc") -> { + CloudFlareHelper.PROTECTION_CAPTCHA + } + // Check for access denied/blocked + bodyString.contains("cf-error-details") -> { + CloudFlareHelper.PROTECTION_BLOCKED + } + + else -> CloudFlareHelper.PROTECTION_NOT_DETECTED + } + } catch (e: Exception) { + e.printStackTraceDebug("CloudFlareInterceptor") + CloudFlareHelper.PROTECTION_NOT_DETECTED + } + } else { + // Standard path for Android 7.0+ + try { + CloudFlareHelper.checkResponseForProtection(response) + } catch (e: IllegalArgumentException) { + if (e.message?.contains("Bad position") == true) { + CloudFlareHelper.PROTECTION_NOT_DETECTED + } else { + e.printStackTraceDebug("CloudFlareInterceptor") + } + } + } + + return when (protectionType) { + CloudFlareHelper.PROTECTION_BLOCKED -> response.closeThrowing( + CloudFlareBlockedException( + url = request.url.toString(), + source = request.tag(MangaSource::class.java), + ), + ) + + CloudFlareHelper.PROTECTION_CAPTCHA -> response.closeThrowing( + CloudFlareProtectedException( + url = request.url.toString(), + source = request.tag(MangaSource::class.java), + headers = request.headers, + ), + ) + + else -> response + } + } - override fun intercept(chain: Interceptor.Chain): Response { - val request = chain.request() - val response = chain.proceed(request) - return when (CloudFlareHelper.checkResponseForProtection(response)) { - CloudFlareHelper.PROTECTION_BLOCKED -> response.closeThrowing( - CloudFlareBlockedException( - url = request.url.toString(), - source = request.tag(MangaSource::class.java), - ), - ) - - CloudFlareHelper.PROTECTION_CAPTCHA -> response.closeThrowing( - CloudFlareProtectedException( - url = request.url.toString(), - source = request.tag(MangaSource::class.java), - headers = request.headers, - ), - ) - - else -> response - } - } - - private fun Response.closeThrowing(error: IOException): Nothing { - try { - close() - } catch (e: Exception) { - error.addSuppressed(e) - } - throw error - } + private fun Response.closeThrowing(error: IOException): Nothing { + try { + close() + } catch (e: Exception) { + error.addSuppressed(e) + } + throw error + } } diff --git a/app/src/main/res/layout/item_extension.xml b/app/src/main/res/layout/item_extension.xml index 55f40b471..2b6d9ddf0 100644 --- a/app/src/main/res/layout/item_extension.xml +++ b/app/src/main/res/layout/item_extension.xml @@ -5,6 +5,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:background="?selectableItemBackground" + android:focusable="true" + android:clickable="true" android:gravity="center_vertical" android:minHeight="?listPreferredItemHeightSmall" android:orientation="horizontal" @@ -15,7 +17,6 @@ android:id="@+id/imageView_icon" android:layout_width="40dp" android:layout_height="40dp" - android:labelFor="@id/textView_title" android:scaleType="fitCenter" tools:src="@tools:sample/avatars" />