Show help banner when Cloudflare captcha page is shown in WebView

This commit is contained in:
arkon 2023-07-28 23:09:52 -04:00
parent cdc1c5efa3
commit 6d69caf59e
4 changed files with 92 additions and 56 deletions

View file

@ -4,8 +4,9 @@ import android.content.pm.ApplicationInfo
import android.graphics.Bitmap import android.graphics.Bitmap
import android.webkit.WebResourceRequest import android.webkit.WebResourceRequest
import android.webkit.WebView import android.webkit.WebView
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@ -17,9 +18,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import com.google.accompanist.web.AccompanistWebViewClient import com.google.accompanist.web.AccompanistWebViewClient
import com.google.accompanist.web.LoadingState import com.google.accompanist.web.LoadingState
@ -28,9 +31,12 @@ import com.google.accompanist.web.rememberWebViewNavigator
import com.google.accompanist.web.rememberWebViewState import com.google.accompanist.web.rememberWebViewState
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.WarningBanner
import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.getHtml
import eu.kanade.tachiyomi.util.system.setDefaultSettings import eu.kanade.tachiyomi.util.system.setDefaultSettings
import kotlinx.coroutines.launch
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
@Composable @Composable
@ -46,7 +52,53 @@ fun WebViewScreenContent(
) { ) {
val state = rememberWebViewState(url = url, additionalHttpHeaders = headers) val state = rememberWebViewState(url = url, additionalHttpHeaders = headers)
val navigator = rememberWebViewNavigator() val navigator = rememberWebViewNavigator()
val uriHandler = LocalUriHandler.current
val scope = rememberCoroutineScope()
var currentUrl by remember { mutableStateOf(url) } var currentUrl by remember { mutableStateOf(url) }
var showCloudflareHelp by remember { mutableStateOf(false) }
val webClient = remember {
object : AccompanistWebViewClient() {
override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
url?.let {
currentUrl = it
onUrlChange(it)
}
}
override fun onPageFinished(view: WebView, url: String?) {
super.onPageFinished(view, url)
scope.launch {
val html = view.getHtml()
showCloudflareHelp = "Checking if the site connection is secure" in html
}
}
override fun doUpdateVisitedHistory(
view: WebView,
url: String?,
isReload: Boolean,
) {
super.doUpdateVisitedHistory(view, url, isReload)
url?.let {
currentUrl = it
onUrlChange(it)
}
}
override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?,
): Boolean {
request?.let {
view?.loadUrl(it.url.toString(), headers)
}
return super.shouldOverrideUrlLoading(view, request)
}
}
}
Scaffold( Scaffold(
topBar = { topBar = {
@ -116,45 +168,21 @@ fun WebViewScreenContent(
} }
}, },
) { contentPadding -> ) { contentPadding ->
val webClient = remember { Column(
object : AccompanistWebViewClient() { modifier = Modifier.padding(contentPadding),
override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
url?.let {
currentUrl = it
onUrlChange(it)
}
}
override fun doUpdateVisitedHistory(
view: WebView,
url: String?,
isReload: Boolean,
) { ) {
super.doUpdateVisitedHistory(view, url, isReload) if (showCloudflareHelp) {
url?.let { WarningBanner(
currentUrl = it textRes = R.string.information_cloudflare_help,
onUrlChange(it) modifier = Modifier.clickable {
} uriHandler.openUri("https://tachiyomi.org/help/guides/troubleshooting/#solving-cloudflare-issues")
} },
)
override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?,
): Boolean {
request?.let {
view?.loadUrl(it.url.toString(), headers)
}
return super.shouldOverrideUrlLoading(view, request)
}
}
} }
WebView( WebView(
state = state, state = state,
modifier = Modifier modifier = Modifier.weight(1f),
.padding(contentPadding)
.fillMaxSize(),
navigator = navigator, navigator = navigator,
onCreated = { webView -> onCreated = { webView ->
webView.setDefaultSettings() webView.setDefaultSettings()
@ -174,3 +202,4 @@ fun WebViewScreenContent(
) )
} }
} }
}

View file

@ -44,7 +44,7 @@ class CloudflareInterceptor(
// Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that // Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that
// we don't crash the entire app // we don't crash the entire app
catch (e: CloudflareBypassException) { catch (e: CloudflareBypassException) {
throw IOException(context.getString(R.string.information_cloudflare_bypass_failure)) throw IOException(context.getString(R.string.information_cloudflare_bypass_failure), e)
} catch (e: Exception) { } catch (e: Exception) {
throw IOException(e) throw IOException(e)
} }

View file

@ -6,8 +6,10 @@ import android.content.pm.PackageManager
import android.webkit.CookieManager import android.webkit.CookieManager
import android.webkit.WebSettings import android.webkit.WebSettings
import android.webkit.WebView import android.webkit.WebView
import kotlinx.coroutines.suspendCancellableCoroutine
import logcat.LogPriority import logcat.LogPriority
import tachiyomi.core.util.system.logcat import tachiyomi.core.util.system.logcat
import kotlin.coroutines.resume
object WebViewUtil { object WebViewUtil {
const val SPOOF_PACKAGE_NAME = "org.chromium.chrome" const val SPOOF_PACKAGE_NAME = "org.chromium.chrome"
@ -32,6 +34,10 @@ fun WebView.isOutdated(): Boolean {
return getWebViewMajorVersion() < WebViewUtil.MINIMUM_WEBVIEW_VERSION return getWebViewMajorVersion() < WebViewUtil.MINIMUM_WEBVIEW_VERSION
} }
suspend fun WebView.getHtml(): String = suspendCancellableCoroutine {
evaluateJavascript("document.documentElement.outerHTML") { html -> it.resume(html) }
}
@SuppressLint("SetJavaScriptEnabled") @SuppressLint("SetJavaScriptEnabled")
fun WebView.setDefaultSettings() { fun WebView.setDefaultSettings() {
with(settings) { with(settings) {

View file

@ -923,6 +923,7 @@
<string name="information_empty_category">You have no categories. Tap the plus button to create one for organizing your library.</string> <string name="information_empty_category">You have no categories. Tap the plus button to create one for organizing your library.</string>
<string name="information_empty_category_dialog">You don\'t have any categories yet.</string> <string name="information_empty_category_dialog">You don\'t have any categories yet.</string>
<string name="information_cloudflare_bypass_failure">Failed to bypass Cloudflare</string> <string name="information_cloudflare_bypass_failure">Failed to bypass Cloudflare</string>
<string name="information_cloudflare_help">Tap here for help with Cloudflare</string>
<string name="information_required_plain">*required</string> <string name="information_required_plain">*required</string>
<!-- Do not translate "WebView" --> <!-- Do not translate "WebView" -->
<string name="information_webview_required">WebView is required for Tachiyomi</string> <string name="information_webview_required">WebView is required for Tachiyomi</string>